开发流程
项目介绍
- 天天生鲜项目是一个表b2c的电商网站,售卖海鲜产品。用户可以完成从注册登录,浏览商品页面,查看商品详情页面,评论,将商品添加到购物车,完成购买,查看个人中心等一系列操作,是一个具备完整功能的电商网站。
- 用户访问流程
- 域名->Nginx服务器->UWSGI服务器->网页内容
- 模块
- 用户模块
- 实现的页面:
- register.html:注册页面
- login.html:登陆页面
- user_center_info.html:用户中心—个人信息页面
- user_center_order.html:用户中心-订单详情页面
- user_center_site.html :用户中心-收货地址页面
- 实现的功能:
- 注册
- 检查用户名是否存在
- 登录
- 显示用户个人信息
- 添加用户收件地址
- 查询用户订单信息
- 展示用户最近浏览商品信息
- 实现的页面:
- 商品模块
- 实现的页面:
- index.html:主页面
- list.html:列表页面
- details.html:商品详情页面
- 实现的功能:
- 展示首页商品
- 获取某个上品的详情信息并展示
- 获取某一大类上品信息并分页展示
- 获取最近商品信息并展示
- 将商品添加到购物车
- 实现的页面:
- 购物车模块
- 实现的页面
- cart.html:购物车页面
- 实现的功能
- 获取用户购物车中的商品信息并展示
- 对购物车中的商品进行编辑
- 提交用户想要购买的商品信息并转到提交订单页面
- 实现的页面
- 订单模块
- 实现的页面:
- place_order.html:确认订单页面
- 实现的功能
- 展示用户选中的商品信息
- 提交订单
- 实现的页面:
- 支付模块
- 链接第三方接口
- 实现的功能
- 调用第三方接口,返回支付结果,修改订单状态
- 用户模块
- 部署
- nginx+UWSGI
- 后端服务
- 数据服务
- MySQL:主从同步,双机热备
- Redis:session,缓存
- FastDFS :分布式存储服务
- 异步消息处理
- celery :异步服务
数据库设计
- 利用Djangon内嵌的ORM框架,利用面向对象的方式创建操作数据库,一个模型类映射一张数据表
- 使用MySQL数据库进行存储
需要设计的数据库表
- 用户模块
- 用户表
- 使用Django自带的用户认证系统管理
- 用户地址表
- 用户表
- 商品模块
- 商品类别表
- 商品SPU表
- 商品SKU表
- 商品图片表
- 主页轮播商品展示表
- 主页分类上品展示表
- 主页促销活动展示表
- 订单模块
- 订单信息表
- 订单商品表
- 购物车模块数据存储在redis中
项目准备
创建项目
- 1.创建应用并注册
- 有四个应用:users,goods,cart,orders
- 2.mysql配置
- 3.定义模型并迁移
- 注意users应用中的模型类User使用的是Django自带的用户认证系统维护的
准备静态文件
- 创建static文件夹,文件夹下有css,js,images,文件夹,和所有的HTML页面
- 可以将前端写好的静态文件都放在相应的文件夹了
配置静态文件的加载路径
STATIC_URL = '/static/' STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
get和post请求
在注册页面,请求注册页面是一个get请求,填写好注册信息,点击提交按钮,是一个post请求
处理注册页面的视图函数
def register(request): """处理注册""" # 获取请求方法,判断是GET/POST请求 if request.method == 'GET': # 处理GET请求,返回注册页面 return render(request, 'register.html') else: # 处理POST请求,实现注册逻辑 return HttpResponse('这里实现注册逻辑')
类视图
- 将视图以类的形式定义
需要继承自通用类试图基类 View
示例:定义类视图处理注册逻辑
users/views.py:
class RegisterView(View):
"""类视图:处理注册"""
def get(self, request):
"""处理GET请求,返回注册页面"""
return render(request, 'register.html')
def post(self, request):
"""处理POST请求,实现注册逻辑"""
return HttpResponse('这里实现注册逻辑')
- 匹配URL
urlpatterns = [
# 类视图:注册
url(r'^register$', views.RegisterView.as_view(), name='register'),
]
- 类试图相对于视图函数,有更高的复用性,其他地方需要用到某个类视图中的某个逻辑,继承该类试图即可
- 正常工作中,以实现逻辑为目标,选择性的使用类试图还是视图函数
模板加载静态文件
- django中加载静态文件的格式和前端的不一样,django是使用static标签加载的
- django擅长处理动态的业务逻辑,静态的业务逻辑交给nginx来处理
注册登录
- 需要实现的逻辑
- 用户注册逻辑
- 用户激活逻辑
- 用户登录逻辑
用户注册逻辑
- 1.发送get请求,返回注册页面
- 2.提交注册信息,post请求,注册成功后跳转到主页面
- 1)获取POST对象中的参数
- 2)判断各个参数是否满足条件,不满足立即返回
- 3)将用户数据存储到数据库
- 4)将用户是否激活设置为false
- 5) 生成激活token(itsdangerous)
- 6)异步发送激活邮件(celery)
- 7)重定向到首页
保存用户信息到数据库
- 为了用户信息的安全,直接使用django提供的用户认证系统完成用户信息的保存
- 调用creat_user(user_name, email, password)实现用户信息的加密保存
- 如果重名,会返回integrityError错误
保存完用户注册信息后,需要重置用户激活状态,因为django用户认证系统默认激活状态为True
关键代码:
# 保存数据到数据库
try:
# 隐私信息需要加密,可以直接使用django提供的用户认证系统完成
user = User.objects.create_user(user_name, email, password)
except db.IntegrityError:
return render(request, 'register.html', {'errmsg': '用户已注册'})
# 手动的将用户认证系统默认的激活状态is_active设置成False,默认是True
user.is_active = False
# 保存数据到数据库
user.save()
用户激活逻辑
- 激活需要发送邮件,是耗时操作,不应该让注册逻辑卡住
- 要实现异步发送激活邮件
- 也就是用户在点击注册按钮后,后台验证完信息,将信息保存到数据库,应该立马将页面跳转到主页面,不能阻塞住。
- 实现异步发送激活邮件的方法就是–使用celery模块
- 实现步骤
- 1.生成激活token
- 2.celery异步发送激活邮件
生成激活token
- 使用itsdangerouse模块
- 步骤
- 1.调用serializer()方法,生成序列化器(传入混淆字符串和过期时间)
- 2.序列化器.dumps(),得到user_id加密后的token(传入封装成字典的user_id)
- 使用loads可以接粗token字符串,得到id明文
代码
User模型类中: from itsdangerous import TimedJSONWebSignatureSerializer as Serializer from django.conf import settings class User(AbstractUser, BaseModel): """用户""" class Meta: db_table = "df_users" def generate_active_token(self): """生成激活令牌""" serializer = Serializer(settings.SECRET_KEY, 3600) token = serializer.dumps({"confirm": self.id}) # 返回bytes类型 return token.decode() 视图函数中调用: token = user.generate_active_token()
celery异步发送邮件
django发送邮件功能
- django中内置了邮件发送的功能
- 在django.core.mail模块中。发邮件的方法是:send_mail()
发邮件流程:
django内部发邮件设置步骤
- 1.确定邮件服务器
- 2.Django中配置邮件服务器参数
- 3.调用send_mail()发送邮件
celery
- celery是一个功能完备即插即用的任务队列
- 是一种跨线程,跨机器工作的机制
- 组成结构:
- 客户端(client)
- 任务队列(broker)
- 任务处理者(worker)
- 执行流程:
- 客户端定义一些任务,如发邮件,将它们放入到中间人(broker)里,需要执行的时候,使用delay()方法,worker就会开始异步执行任务并且返回执行的结果。
特点
- 简单,易于使用和维护,文档丰富易懂
- 高效,单个celery进程每分钟可以处理数百万个任务
- 灵活,celery每个部分都可以自定义拓展
- celery非常易于集成到一些web框架中
使用步骤
- 在项目根目录下创建celery_tasks文件夹,文件夹下创建tasks.py
- 在tasks.py中创建celery应用对象,app
- 定义需要异步处理的任务,用装饰器@app.task装饰
在视图函数中,在需要异步处理某个耗时操作的时候,调用该任务函数的时候加上delay,完成异步任务处理
代码
tasks.py: from celery import Celery from django.core.mail import send_mail from django.conf import settings # 创建celery应用对象 app = Celery('celery_tasks.tasks', broker='redis://192.168.243.191:6379/4') @app.task def send_active_email(to_email, user_name, token): """发送激活邮件""" subject = "天天生鲜用户激活" # 标题 body = "" # 文本邮件体 sender = settings.EMAIL_FROM # 发件人 receiver = [to_email] # 接收人 html_body = '<h1>尊敬的用户 %s, 感谢您注册天天生鲜!</h1>' \ '<br/><p>请点击此链接激活您的帐号<a href="http://127.0.0.1:8000/users/active/%s">' \ 'http://127.0.0.1:8000/users/active/%s</a></p>' %(user_name, token, token) send_mail(subject, body, sender, receiver, html_message=html_body) 注册视图函数: # 生成激活token token = user.generate_activate_token() # 发送激活邮件 send_active_email.delay(email, user_name, token) # 响应给用户,注册后重定向到主页,也不一定到主页,根据公司需求而定 return redirect(reverse('goods:index'))
worker
- worker是用读取队列任务,执行任务的
实现步骤
- 1.重新拷贝一份代码到celery服务器上
- 2.在celery_tasks/tasks.py文件顶部添加以下代码
import os os.environ["DJANGO_SETTINGS_MODULE"] = "dailyfresh.settings" # 放到Celery服务器上时添加的代码 import django django.setup()
- 3.终端创建worker:
celery -A celery_tasks.tasks worker -l info
- 4.开启redis-server
5.测试发邮件
- 完整代码:
class RegisterView(View):
"""类视图:处理注册"""
def get(self, request):
"""处理GET请求,返回注册页面"""
return render(request, 'register.html')
def post(self, request):
"""处理POST请求,实现注册逻辑"""
# 获取注册请求参数
user_name = request.POST.get('user_name')
password = request.POST.get('pwd')
email = request.POST.get('email')
allow = request.POST.get('allow')
# 参数校验:缺少任意一个参数,就不要在继续执行
if not all([user_name, password, email]):
return redirect(reverse('users:register'))
# 判断邮箱
if not re.match(r"^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$", email):
return render(request, 'register.html', {'errmsg':'邮箱格式不正确'})
# 判断是否勾选协
if allow != 'on':
return render(request, 'register.html', {'errmsg': '没有勾选用户协议'})
# 保存数据到数据库
try:
# 隐私信息需要加密,可以直接使用django提供的用户认证系统完成
user = User.objects.create_user(user_name, email, password)
except db.IntegrityError:
return render(request, 'register.html', {'errmsg': '用户已注册'})
# 手动的将用户认证系统默认的激活状态is_active设置成False,默认是True
user.is_active = False
# 保存数据到数据库
user.save()
# 生成激活token
token = user.generate_active_token()
# celery发送激活邮件:异步完成,发送邮件不会阻塞结果的返回
send_active_email.delay(email, user_name, token)
# 返回结果:比如重定向到首页
return redirect(reverse('goods:index'))
激活逻辑
- 当用户点击激活链接时,逻辑如下
- 1.创建序列化器,解析出返回的token,判断是否过期,如果没有过期,获取user_id
- 2.判断用户是否存在,不存在为黑客攻击,返回
- 3.如果存在,修改is_active属性为True
- 代码:
class ActiveView(View):
"""用户激活"""
def get(self, request, token):
# 创建序列化器
serializer = Serializer(settings.SECRET_KEY, 3600)
try:
# 使用序列化器,获取token明文信息,需要判断签名是否过期
result = serializer.loads(token)
except SignatureExpired:
# 提示激活链接已过期
return HttpResponse('激活链接已过期')
# 获取用户id
user_id = result.get('confirm')
try:
# 查询需要激活的用户,需要判断查询的用户是否存在
user = User.objects.get(id=user_id)
except User.DoesNotExist:
# 提示用户不存在
return HttpResponse('用户不存在')
# 设置激活用户的is_active为Ture
user.is_active = True
# 保存数据到数据库
user.save()
# 响应信息给客户端
return redirect(reverse('users:login'))
用户登录逻辑
- 流程
- 1.点击登录,get请求,返回登陆页面
- 2.用户输入的登录信息(用户名和密码),post请求将登录信息传入到后端
- 3.参数验证
- 4.调用django自带的用户认证系统authenticate,判断是否登陆成功
- 5.判断用户是否激活
- 6.使用django用户认证系统的的login方法,在session中保存用户的登录状态
- 7.返回重定向页面
class LoginView(View):
"""登陆"""
def get(self, request):
"""响应登陆页面"""
return render(request, 'login.html')
def post(self, request):
"""处理登陆逻辑"""
# 获取用户名和密码
user_name = request.POST.get('username')
password = request.POST.get('pwd')
# 参数校验
if not all([user_name, password]):
return redirect(reverse('users:login'))
# django用户认证系统判断是否登陆成功
user = authenticate(username=user_name, password=password)
# 验证登陆失败
if user is None:
# 响应登录页面,提示用户名或密码错误
return render(request, 'login.html', {'errmsg':'用户名或密码错误'})
# 验证登陆成功,并判断是否是激活用户
if user.is_active == False:
# 如果不是激活用户
return render(request, 'login.html', {'errmsg':'用户未激活'})
# 使用django的用户认证系统,在session中保存用户的登陆状态
login(request, user)
# 登陆成功,重定向到主页
return redirect(reverse('goods:index'))
状态保持
- 用户登陆成功后,需要将用户的登录状态记录下来,即伯村有服务器生成的session数据
浏览器和服务器都要保存用户登录状态
- 服务器存储session数据
- 浏览器存储sessionid
如何需要将服务器的session数据存储在redis中,需要配合
django—redis模块
- 步骤
- 1.安装django-redis:
pip install django-redis
- 2.settings.py中配置django-redis
- 1.安装django-redis:
登录记住用户
为什么要记住用户
- 不记住用户:用户打开网页-》登录-》关闭网页-》打开网页-》登录–麻烦
- 记住用户:用户打开网页-》登录-》关闭网页-》打开网页-》直接登录状态–方便
技术分析
- 服务器redis数据库中存储了用户的session信息
- 浏览器cookie中,存储了用户的sessonid信息
- 每次请求,浏览器都会带上完整的cookies信息给服务器
- 则:只要更改session的过期时间即可,
request.session.set_expiry(value)
- value:整数,表示value秒后过期
- value:0,表示浏览器关闭后过期
- value:none:表示两个星期后过期
代码:
# 获取是否勾选'记住用户名' remembered = request.POST.get('remembered') # 判断是否是否勾选'记住用户名' if remembered != 'on': # 没有勾选,不需要记住cookie信息,浏览器关闭即过期 request.session.set_expiry(0) else: # 已勾选,需要记住cookie信息,两周后过期 request.session.set_expiry(None)
退出登录
class LogoutView(View):
"""退出登录"""
def get(self, request):
"""处理退出登录逻辑"""
# 由Django用户认证系统完成:需要清理cookie和session,request参数中有user对象
logout(request)
# 退出后跳转:由产品经理设计
return redirect(reverse('goods:index'))
- get请求
- 使用django用户系统提供的logout()函数
- 函数内部会进行清除session和cookie的操作
用户中心
用户是否登录的验证
- 要想进入用户中心页面,那么这个用户就必须要登录,怎么验证登录呢?
使用django内置的login_required方法
- 该方法可以验证用户是否通过登录验证,如果已经登录,则返回True,否则返回False
- 在django.contrib.auth.decorator中
使用步骤
- 1.主目录下的utils(工具集)中的views.py(有关于视图的工具类)中创建类loginRequireMixin类
class LoginRequiredMixin(object): @classmethod def as_view(cls, **initkwargs): view = super().as_view(**initkwargs) return login_required(view)
- 2.在需要验证的类视图里,继承loginRequriedMixin类即可
next参数
- 用来标记,用户如果登陆或者访问目标页面失败,将要定向到的页面,如用户没有登陆就想访问用户信息页面,就会使用next参数重定向到该页面,具体重定向到哪个页面,由产品径流说了算
用户地址页
- 页面分析:有get请求和post请求
- get:根据根据获取到的用户ID从数据库中获取该用户的地址地址信息,放在上下文,传给模板,以一定的顺序(创建时间) 渲染排列出来
- post:用户想要添加新的地址,使用表单提交,提交后,后端,验证接收数据,验证参数,将地址保存到数据库,重定向到地址页面,get请求
个人信息页面
- 展示用户基本信息(get请求)和最近浏览记录
最近浏览记录
- 从redis中取出
- 步骤
- 创建redis链接对象,
redis_connection = get_redis_connection('default')
- 根据需求的用户id,获取用户浏览上品的sku_id列表,
sku_ids = redis_connection.lrange('history_%s'%user_id,0,4)
- 从数据库中查询出sku_ids对用的sku_list
- 存储到上下文中返回
- 创建redis链接对象,
- 代码:
class UserInfoView(LoginRequiredMixin, View): """用户中心""" def get(self, request): """查询用户信息和地址信息""" # 从request中获取user对象,中间件从验证请求中的用户,所以request中带有user user = request.user try: # 查询用户地址:根据创建时间排序,取第1个地址 address = user.address_set.latest('create_time') except Address.DoesNotExist: # 如果地址信息不存在 address = None # 创建redis连接对象 redis_connection = get_redis_connection('default') # 从Redis中获取用户浏览商品的sku_id,在redis中需要维护商品浏览顺序[8,2,5] sku_ids = redis_connection.lrange('history_%s'%user.id, 0, 4) # 从数据库中查询商品sku信息,范围在sku_ids中 # skuList = GoodsSKU.objects.filter(id__in=sku_ids) # 问题:经过数据库查询后得到的skuList,就不再是redis中维护的顺序了,而是[2,5,8] # 需求:保证经过数据库查询后,依然是[8,2,5] skuList = [] for sku_id in sku_ids: sku = GoodsSKU.objects.filter(id=sku_id) skuList.append(sku) # 构造上下文 context = { 'address':address, 'skuList':skuList, } # 调出并渲染模板 return render(request, 'user_center_info.html', context)
商品模块
FastDFS
- 分布式文件存储系统,方便的进行网站图片等信息的存储
- 解决了大容量存储和负载均衡的问题
- 适合存储图片,视屏等
组成
- client:上传下载的请求者
- tracker:负载均衡存储器,负责对存储器的调度
- storage:存储信息的结构
步骤
- 上传
- client询问tracker,有上传文件的需求
- tracker返回一台可用的storage
- client直接和storage进行通讯,完成文件的存储
- 下载
- client询问tracker,有下载文件的需求
- tracker返回一台可用的storage
- client直接和storage通讯完成文件的下载
- 上传
主商品页面
- 数据分析
- 个人信息
- 商品分类信息
- 图片轮播图信息
- 活动信息
- 少年宫品SKU信息
- 购物车信息
- 当用户访问主页/跳转到主页时,get请求,会在数据库中找到以上信息,填充模板,返还给浏览器
页面静态化
- 主页,信息丰富,需要多次查询,才能获取到全部信息,这样就会反应很慢
解决办法–页面静态化,通过Nginx访问主页
- 将主页的html页面先存储起来,需要访问的时候,直接返回完整的HTMl页面
- 实现思路
- 后台站点在发布主页内容(增删改),Django使用异步任务的方式生成静态页面
- 其中:
- 使用celery服务器执行异步任务
- 使用Nginx服务器提供静态页面的访问
- 需要注册模型类到站点,并且创建模型类管理类,在模型类管理类中调用celery异步任务
实现步骤
- 1.定义异步任务
@app.task def generate_static_index_html(): """生成静态的html页面""" # 查询商品分类信息 categorys = GoodsCategory.objects.all() # 查询图片轮播信息:按照index进行排序 banners = IndexGoodsBanner.objects.all().order_by('index') # 查询活动信息 promotion_banners = IndexPromotionBanner.objects.all().order_by('index') # 查询分类商品信息 for category in categorys: title_banners = IndexCategoryGoodsBanner.objects.filter(category=category, display_type=0).order_by('index') category.title_banners = title_banners image_banners = IndexCategoryGoodsBanner.objects.filter(category=category, display_type=1).order_by('index') category.image_banners = image_banners print(category.image_banners[0].sku.default_image.url) # 查询购物车信息 cart_num = 0 # 构造上下文 context = { 'categorys': categorys, 'banners': banners, 'promotion_banners': promotion_banners, 'cart_num': cart_num } # 加载模板 template = loader.get_template('static_index.html') html_data = template.render(context) # 保存成html文件:放到静态文件中 file_path = os.path.join(settings.STATICFILES_DIRS[0], 'index.html') with open(file_path, 'w') as file: file.write(html_data)
- 2.配置Nginx访问静态页面
- 3.模型管理类调用celery异步方法
- 这是生成静态页面的发起点
- 管理员在通过站点发布内容时,在这里会调用celery异步方法,生成HTML静态页面
class BaseAdmin(admin.ModelAdmin): """商品活动信息的管理类,运营人员在后台发布内容时,异步生成静态页面""" def save_model(self, request, obj, form, change): """后台保存对象数据时使用""" # obj表示要保存的对象,调用save(),将对象保存到数据库中 obj.save() # 调用celery异步生成静态文件方法 generate_static_index_html.delay() def delete_model(self, request, obj): """后台保存对象数据时使用""" obj.delete() generate_static_index_html.delay() class IndexPromotionBannerAdmin(BaseAdmin): """商品活动站点管理,如果有自己的新的逻辑也是写在这里""" # list_display = [] pass class GoodsCategoryAdmin(BaseAdmin): pass class GoodsAdmin(BaseAdmin): pass class GoodsSKUAdmin(BaseAdmin): pass class IndexCategoryGoodsBannerAdmin(BaseAdmin): pass # Register your models here. admin.site.register(GoodsCategory,GoodsCategoryAdmin) admin.site.register(Goods,GoodsAdmin) admin.site.register(GoodsSKU,GoodsSKUAdmin) admin.site.register(IndexPromotionBanner,IndexPromotionBannerAdmin) admin.site.register(IndexCategoryGoodsBanner,IndexCategoryGoodsBannerAdmin)
缓存
- 用户未登录访问,主页,使用静态页面的方式,加快加载速度,用户登录后,访问主页,就要是动态访问,这种情况下,速度还是会很慢,怎么解决?–缓存
- 缓存时对动态查询数据的一种存储,可以存储在系统内存中,也可以存储在缓存数据库中
- 用户在访问页面时,会先尝试获取缓存数据,如果获取到,直接返回数据,获取不到,就查询数据库
- 存储进去的是什么,获取到的就是什么
关键代码:
# 模块: from django.core.cache import cache # 设置缓存: cache.set('key', 内容, 有效期) # 读取缓存 cache.get('key') # 删除缓存 cache.delete('key')
代码(缓存主页数据)
class IndexView(View): """首页""" def get(self, request): """查询首页页面需要的数据,构造上下文,渲染首页页面""" # 查询用户个人信息(request.user) # 先从缓存中读取数据,如果有就获取缓存数据,反之,就执行查询 context = cache.get('index_page_data') if context is None: print('没有缓存数据,查询了数据库') # 查询商品分类信息 categorys = GoodsCategory.objects.all() # 查询图片轮播信息:按照index进行排序 banners = IndexGoodsBanner.objects.all().order_by('index') # 查询活动信息 promotion_banners = IndexPromotionBanner.objects.all().order_by('index') # 查询分类商品信息 for category in categorys: title_banners = IndexCategoryGoodsBanner.objects.filter(category=category, display_type=0).order_by('index') category.title_banners = title_banners image_banners = IndexCategoryGoodsBanner.objects.filter(category=category, display_type=1).order_by('index') category.image_banners = image_banners # 构造上下文:先处理购物车以外的上下文,并缓存 context = { 'categorys':categorys, 'banners':banners, 'promotion_banners':promotion_banners, } # 设置缓存数据:名字,内容,有效期 cache.set('index_page_data',context,3600) # 查询购物车信息:不能被缓存,因为会经常变化 cart_num = 0 # 补充购物车数据 context.update(cart_num=cart_num) return render(request, 'index.html',context)
- 注意:
- 1.缓存需要设置有效期,不然数据永远无法得到更新,具体的有效期时间根据公司需求而定
2.缓存在修改内容时需要删除,不然内容修改了,缓存的还是旧内容
class BaseAdmin(admin.ModelAdmin): """商品活动信息的管理类,运营人员在后台发布内容时,异步生成静态页面""" def save_model(self, request, obj, form, change): """后台保存对象数据时使用""" # obj表示要保存的对象,调用save(),将对象保存到数据库中 obj.save() # 调用celery异步生成静态文件方法,操作完表单后删除静态文件 generate_static_index_html.delay() # 修改了数据库数据就需要删除缓存 cache.delete('index_page_data') def delete_model(self, request, obj): """后台保存对象数据时使用""" obj.delete() generate_static_index_html.delay() cache.delete('index_page_data')
购物车数据
- 对于用户的购物车浏览记录,因为一直在变化,所以不能存储在缓存中,那么存储在哪里?
- redis中,因为redis是一种存储在内存中的数据库,快捷方便
存储方式
- 每个用户用一条购物车数据维护
- 使用哈希类型
步骤
- 1.先判断用户是否登录,登陆了,才能获取到购物车数据
- 2.创建django-redis中的redis_conn对象
- 3.获取用户id
- 4.从redis中获取购物车数据,返回字典
- 5.遍历购物车字典的值,累加购物车内的商品的值(用于页面显示)
# 如果用户登录,就获取购物车数据 if request.user.is_authenticated(): # 创建redis_conn对象 redis_conn = get_redis_connection('default') # 获取用户id user_id = request.user.id # 从redis中获取购物车数据,返回字典 cart_dict = redis_conn.hgetall('cart_%s'%user_id) # 遍历购物车字典的值,累加购物车的值 for value in cart_dict.values(): cart_num += int(value)
商品详情页
- 参数:需要商品的sku_id
- 查询的内容
- 查询上品的SKU信息
- 查询所有商品的分类信息
- 查询商品的订单评论信息
- 查询最新商品推荐
- 查询其他规格商品
- 如果已经登录,查询购物车信息
- 注意
- 检查是否有缓存,有缓存,使用缓存
- 在商品详情页需要实现存储浏览记录的逻辑(存储在redis中)
class DetailView(View):
"""商品详细信息页面"""
def get(self, request, sku_id):
# 尝试获取缓存数据
context = cache.get("detail_%s" % sku_id)
# 如果缓存不存在
if context is None:
try:
# 获取商品信息
sku = GoodsSKU.objects.get(id=sku_id)
except GoodsSKU.DoesNotExist:
# from django.http import Http404
# raise Http404("商品不存在!")
return redirect(reverse("goods:index"))
# 获取类别
categorys = GoodsCategory.objects.all()
# 从订单中获取评论信息
sku_orders = sku.ordergoods_set.all().order_by('-create_time')[:30]
if sku_orders:
for sku_order in sku_orders:
sku_order.ctime = sku_order.create_time.strftime('%Y-%m-%d %H:%M:%S')
sku_order.username = sku_order.order.user.username
else:
sku_orders = []
# 获取最新推荐
new_skus = GoodsSKU.objects.filter(category=sku.category).order_by("-create_time")[:2]
# 获取其他规格的商品
other_skus = sku.goods.goodssku_set.exclude(id=sku_id)
context = {
"categorys": categorys,
"sku": sku,
"orders": sku_orders,
"new_skus": new_skus,
"other_skus": other_skus
}
# 设置缓存
cache.set("detail_%s"%sku_id, context, 3600)
# 购物车数量
cart_num = 0
# 如果是登录的用户
if request.user.is_authenticated():
# 获取用户id
user_id = request.user.id
# 从redis中获取购物车信息
redis_conn = get_redis_connection("default")
# 如果redis中不存在,会返回None
cart_dict = redis_conn.hgetall("cart_%s"%user_id)
for val in cart_dict.values():
cart_num += int(val)
# 浏览记录: lpush history_userid sku_1, sku_2
# 移除已经存在的本商品浏览记录
redis_conn.lrem("history_%s"%user_id, 0, sku_id)
# 添加新的浏览记录
redis_conn.lpush("history_%s"%user_id, sku_id)
# 只保存最多5条记录
redis_conn.ltrim("history_%s"%user_id, 0, 4)
context.update({"cart_num": cart_num})
return render(request, 'detail.html', context)
商品列表页
分析
- 需要知道展示是哪一类商品
- 需要展示的是第几页
- 需要知道排序的规则(默认?价格?人气?)
- 请求方法是get,只需要获取数据
- 传递参数(排序规则)到视图中,需要进行参数校验
需要查询的数据
- 购物车数据
- 上品分类信息
- 新推荐信息,在GoodsSKU表中,查询特定类别信息,按照时间倒序
- 商品列表信息
- 上品分页信息
参数传递方式
- 展示某商品第几页的数据,然后再排序
/list/category_id/page_num/?sort='默认,价格,人气'
提示
- 1.获取请求参数信息:商品id,第几页数据 ,排序规则
- 2.校验参数
- 1)判断类别是否存在,查询数据库验证,如果不存在,异常:
GoodCategory.DoesNotExist
- 2)分页的异常,在创建分页对象时校验
- 因为只有创建了分页数据,才能知道页数page是否正确
- 如果页数错误,异常为:
EmptyPage
- 1)判断类别是否存在,查询数据库验证,如果不存在,异常:
class ListView(View):
"""商品列表"""
def get(self, request, category_id, page_num):
# 获取sort参数:如果用户不传,就是默认的排序规则
sort = request.GET.get('sort', 'default')
# 校验参数
# 判断category_id是否正确,通过异常来判断
try:
category = GoodsCategory.objects.get(id=category_id)
except GoodsCategory.DoesNotExist:
return redirect(reverse('goods:index'))
# 购物车
cart_num = 0
# 如果是登录的用户
if request.user.is_authenticated():
# 获取用户id
user_id = request.user.id
# 从redis中获取购物车信息
redis_conn = get_redis_connection("default")
# 如果redis中不存在,会返回None
cart_dict = redis_conn.hgetall("cart_%s" % user_id)
for val in cart_dict.values():
cart_num += int(val)
# 查询商品所有类别
categorys = GoodsCategory.objects.all()
# 查询该类别商品新品推荐
new_skus = GoodsSKU.objects.filter(category=category).order_by('-create_time')[:2]
# 查询该类别所有商品SKU信息:按照排序规则来查询
if sort == 'price':
# 按照价格由低到高
skus = GoodsSKU.objects.filter(category=category).order_by('price')
elif sort == 'hot':
# 按照销量由高到低
skus = GoodsSKU.objects.filter(category=category).order_by('-sales')
else:
skus = GoodsSKU.objects.filter(category=category)
# 无论用户是否传入或者传入其他的排序规则,我在这里都重置成'default'
sort = 'default'
# 分页:需要知道从第几页展示
page_num = int(page_num)
# 创建分页器:每页两条记录
paginator = Paginator(skus,2)
# 校验page_num:只有知道分页对对象,才能知道page_num是否正确
try:
page_skus = paginator.page(page_num)
except EmptyPage:
# 如果page_num不正确,默认给用户第一页数据
page_skus = paginator.page(1)
# 获取页数列表
page_list = paginator.page_range
# 构造上下文
context = {
'sort':sort,
'category':category,
'cart_num':cart_num,
'categorys':categorys,
'new_skus':new_skus,
'page_skus':page_skus,
'page_list':page_list
}
# 渲染模板
return render(request, 'list.html', context)
商品搜索
搜素引擎和框架
- whoosh
- 纯python编写的全文搜索引擎
- 性能比不上sphinx,xapian,Elasticsearc,但是无二进制包,程序不会莫名奇妙的崩溃,对于小型站点,whoosh已经够用
- haystack
- 全文检索框架,支持whoosh,solr,Xapian,Elasticsearc四种全文检索引擎
- 作用:搭建了用户和搜索引擎之间的桥梁
- jieba
- 免费的中文分词包,用来对输入的中文关键字进行分词处理
- whoosh
步骤
- 配置全文检索
- 1.pip安装 django-haystack,whoosh,jieba
- 2.在settings.py中的INSTALLED_APPS配置项中,添加‘haystack‘;配置haystack和whoosh的链接
- 定义商品索引类
- 指定要建立索引的字段
- 生成索引文件
- 搜索表单处理
- 配置搜索地址正则
- 配置全文检索
购物车
- 使用redis数据库存储购物车数据
- 购物车里需要完成增删改查的操作
- 查询的结果,需要由服务器响应界面给客户端展示出来
- 增删改的操作,是客户端发数据给服务器,连着之间的交互是局部刷新的效果,需要使用ajax
- 添加购物车的请求方法:POST
- 服务器和客户端的数据传输格式:json
服务器接收到的数据
- 用户id:user_id
- 商品id:sku_id
- 商品数量:count
当用户已登录时,将购物车数据存储到服务器的redis中
- 当用户未登录时,将购物车数据存储到浏览器的cookie中
- 使用json字符串将购物车数据保存到浏览器的cookie中
- 每个人的浏览器cookie存储的都是个人的购物车数据,所以key不用唯一标示
当用户进行登录是,将cookie中的购物车数据合并到redis中
由于主页,详情页,列表页,中,都涉及到购物车的数据展示,所以将购物车逻辑封装到BaseCartView中
登录时,购物车合并cookie和redis
- 在登陆页面跳转前,将cookie和redis的购物车数据合并到redis中
- 步骤:
- 获取cookie中的购物车数据
- 获取redis中的购物车数据
- 合并购物车上商品数量信息
- 如果cookie中存在,redis中也有,则进行数量累加
- 如果cookie中存在的,redis中没有,则生成新的购物车数据
- 将cookie中的购物车数据合并到redis中
- 清除浏览器购物车cookie
订单
- 页面入口
- 点击详情页的立即购买进入订单确认页面
- 点击购物车的去结算 进入订单确认页面
- 点击订单确认页面的提交订单进入全部订单页面
- 点击全部订单的去付款进入支付宝
提交订单之事务支持
- Django中的数据库,默认是自动提交的
- 当OrderInfo和OrderGoods保存数据时,如果出现异常,需要执行回滚,不要自动提交
- 保存数据时,只有当没有任何错误时,才能完成数据的保存
要么一起成功,要么一起失败
实现步骤
- 定义TransactionAtomicMixin类
- 在类试图中继承 transactionAtomcMixin类
- 在创建数据库祈前创建事务保存点:
save_point = transaction.savepoint()
- 出现异常的地方都回滚到事务保存点
- 当数据库操作结束,还没有异常时,才能提交事务:
transaction.savepoint_commit(savepoint)
提交订单之并发和锁
?:多线程和多进程访问共享资源时,容易出现资源抢夺的问题
- 解决办法:加锁(悲观锁/乐观锁)
- 悲观锁
- 当要操作某条记录时,立即将该条记录锁起来,谁也无法操作
- select * from table where id=17 for update;
乐观锁
- 在查询数据的时候不加锁,在更新时进行判断
- 判断更新时的库存和之前查出来的库存是否一致
- update table set set stock=2 where id=17 and stock=7;
代码:没有使用锁
# 减少sku库存 sku.stock -= sku_count # 增加sku销量 sku_sales += sku_count sku.save()
- 代码:使用乐观锁
# 查询当前库存量 origin_stock = sku.stock # 减少库存,增加销量 new_stock = origin_stock - sku_count new_sales = sku_sales + sku_count # 更新库存和销量 result = GoodsSKU.objects.filter(id=sku_id, stock=origin_stock).update(stock=new_stock, sales=new_sales)
支付宝
提交订单到支付宝
- 点击确认支付按钮,跳转到支付宝支付页面
- 步骤
- 用户向服务器传递订单id,当做请求参数传入
- 服务器接收订单id,并校验订单id
- 获取要支付的订单的信息,order,并校验
- 创建用于对接支付宝的支付的对象,alipay
- 创建支付接口对应的请求信息,order_string
- 生成访问支付宝的支付地址
class PayView(LoginRequiredJSONMixin, View):
"""订单支付"""
def post(self, request):
# 订单id
# 校验订单
# 获取订单信息
# 创建用于支付宝支付的对象
# 电脑网站支付,需要跳转到https://openapi.alipay.com/gateway.do? + order_string
# 生成url:让用户进入支付宝页面的支付网址
# 响应结果
准备url
# 支付请求
url(r'^pay$', views.PayView.as_view(), name='pay')
准备视图
class PayView(LoginRequiredJSONMixin, View):
"""订单支付"""
def post(self, request):
# 订单id
order_id = request.POST.get('order_id')
# 校验订单
if not order_id:
return JsonResponse({'code': 2, 'message': '订单id错误'})
# 获取订单信息
try:
order = OrderInfo.objects.get(order_id=order_id, user=request.user,
status=OrderInfo.ORDER_STATUS_ENUM["UNPAID"],
pay_method=OrderInfo.PAY_METHODS_ENUM["ALIPAY"])
except OrderInfo.DoesNotExist:
return JsonResponse({'code': 3, 'message': '订单错误'})
# 创建用于支付宝支付的对象
alipay = AliPay(
appid=settings.ALIPAY_APPID,
app_notify_url=None, # 默认回调url
app_private_key_path=os.path.join(settings.BASE_DIR,'apps/orders/app_private_key.pem'),
alipay_public_key_path=os.path.join(settings.BASE_DIR,'apps/orders/alipay_public_key.pem'), # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
sign_type = "RSA2", # RSA 或者 RSA2
debug = True # 默认False 配合沙箱模式使用
)
# 电脑网站支付,需要跳转到https://openapi.alipay.com/gateway.do? + order_string
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no=order_id,
total_amount=str(order.total_amount), # 将浮点数转成字符串
subject='头条生鲜',
return_url=None,
notify_url=None # 可选, 不填则使用默认notify url
)
# 生成url:让用户进入支付宝页面的支付网址
url = settings.ALIPAY_URL + '?' + order_string
return JsonResponse({'code': 0, 'message': '支付成功', 'url':url})
查询支付状态
- 使用支付对象的api_alipay_trade_query(order_id),查询订单完成状态
- 根据返回的状态进行操作,如果正在进行中,继续循环请求,如果支付成功,更改数据库中订单状态信息,返回给前端,订单支付成功,如果订单支付失败,直接返回信息给前端,说明订单支付失败
while True:
# 查询支付结果:返回字典
response = alipay.api_alipay_trade_query(order_id)
# 判断支付结果
code = response.get('code') # 支付宝接口调用结果的标志
trade_status = response.get('trade_status') # 用户支付状态
if code == '10000' and trade_status == 'TRADE_SUCCESS':
# 表示用户支付成功
# 设置订单的支付状态为待评论
order.status = OrderInfo.ORDER_STATUS_ENUM['UNCOMMENT']
# 设置支付宝对应的订单编号
order.trade_id = response.get('trade_no')
order.save()
# 返回json,告诉前端结果
return JsonResponse({'code': 0, 'message': '支付成功'})
elif code == '40004' or (code == '10000' and trade_status == 'WAIT_BUYER_PAY'):
# 表示支付宝的接口暂时调用失败,网络延迟,订单还未生成;or 等待订单的支付
# 继续查询
continue
else:
# 支付失败,返回支付失败的通知
return JsonResponse({'code': 4, 'message': '支付失败'})
部署
- 当项目开发完成后,需要将项目放到服务器上
- 让服务器使用固定的ip,再通过绑定域名,就可以供其他人浏览了
- 对于python web开发,可以使用wsgi,apache服务器
- 部署需要安装的软件:uWSGI,Nginx
- 访问流程
- 浏览器发送请求给Nginx服务器,如果是静态文件,则nginx会直接返回
- 如果不是静态页面,nginx会把请求信息转给uWSGI服务器,uWSGI将请求转给django框架,url路由到相应的视图函数,处理完逻辑,返回响应结果经由uWSGI和nginx在到浏览器
django对接MySQL主从
- 对mysql进行读写分离设置,提高数据库的访问速度
步骤
- 1.增加主从数据库
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'dailyfresh', 'HOST': '192.168.243.193', # MySQL数据库地址(主) 'PORT': '3306', 'USER': 'root', 'PASSWORD': 'mysql', }, 'slave': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'dailyfresh', 'HOST': '192.168.243.189', # MySQL数据库地址(从) 'PORT': '3306', 'USER': 'root', 'PASSWORD': 'mysql', } }
- 2.编辑路由分发的类,在根目录下的utils目录下常见db_router.py
class MasterSlaveDBRouter(object): """读写分离路由""" def db_for_read(self, model, **hints): """读""" return 'slave' def db_for_write(self, model, **hints): """读""" return 'default' def allow_relation(self, obj1, obj2, **hints): """是否允许关联查询""" return True
- 3.读写分离引导:settings.py
# 配置读写分离 DATABASE_ROUTERS = ['utils.db_router.MasterSlaveDBRouter']