1-10_Django项目实战文档
本网站是基于Django+uwsgi+nginx+MySQL+redis+linux+requests开发的电商购物系统,
以及通过使用爬虫技术批量获取商品数据.
实现
客户端: 注册 , 登录 , 浏览记录保存, 购物车 , 订单等功能实现
管理端: 商品添加 , 用户管理等功能
项目内容较多 , 该博文只是对整体的大致思路介绍 , 如有疑问可以私信博主
项目的完整代码可见博主主页上传的资源
项目git地址: https://gitee.com/jixuonline/django_-shop-system
详细介绍: https://blog.csdn.net/xiugtt6141121/category_12658164.html
服务器部署教程: https://blog.csdn.net/xiugtt6141121/article/details/139497427
一、项目环境
python 3.8.10 django 3.2 mysql 5.7.40 redis
二、项目环境的配置
1、创建 Django 项目 —— ShopSystem
2、配置 MySQL 的连接引擎
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'shop_10', 'USER' : 'root', 'PASSWORD' : 'root', 'HOST' : '127.0.0.1' } }
3、配置静态文件项目检索路径
STATIC_URL = '/static/' STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
4、配置内存型数据库,配置 Redis 的连接引擎
# 配置 Redis 缓存数据库信息 CACHES = { # 默认使用的 Redis 数据库 "default":{ # 配置数据库指定引擎 "BACKEND" : "django_redis.cache.RedisCache", # 配置使用 Redis 的数据库名称 "LOCATION" : "redis://127.0.0.1:6379/0", "OPTIONS":{ "CLIENT_CLASS" : "django_redis.client.DefaultClient" } }, # 将 session 的数据保存位置修改到 redis 中 "session":{ # 配置数据库指定引擎 "BACKEND" : "django_redis.cache.RedisCache", # 配置使用 Redis 的数据库名称 "LOCATION" : "redis://127.0.0.1:6379/1", "OPTIONS":{ "CLIENT_CLASS" : "django_redis.client.DefaultClient" } }, } # 修改 session 默认的存储机制 SESSION_ENGINE = "django.contrib.sessions.backends.cache" # 配置 SESSION 要缓存的地方 SESSION_CACHE_ALIAS = "session"
三、响应首页
创建响应首页的应用 —— contents
配置响应路由 ,实现响应的视图
# 响应首页 path('' , views.IndexView.as_view()) class IndexView(View): ''' 响应首页 ''' def get(self , request): return render(request , 'index.html')
四、用户注册
1、实现用户的数据模型类 2、响应注册页面 3、接收用户输入的数据 4、对用户输入的数据进行校验 5、保存用户数据,注册成功
创建应用实现用户逻辑操作 —— users
1、响应注册页面视图
class RegisterView(View): ''' 用户注册 ''' def get(self , request): return render(request , 'register.html')
2、定义用户数据模型类
使用 auth 模块实现保存用户数据,自定义认证模型类别
from django.contrib.auth.models import AbstractUser class User(AbstractUser): mobile = models.CharField(max_length=11 , unique=True) class Meta: db_table = 'user'
修改 Django 全局默认的认证模型类
# 配置自定义模型类 AUTH_USER_MODEL = 'users.User'
3、后端数据校验
实现用户数据提交之后,Django 对数据校验是否合法,自定义 forms 表单类进行校验
在应用中创建 forms 模块
from django import forms class RegisterForm(forms.Form): ''' 校验用户注册提交的数据 ''' username = forms.CharField(min_length= 5 , max_length= 15, error_messages={ "min_length":"用户名过短", "max_length":"用户名过长", "required":"用户名不允许为空" }) password = forms.CharField(min_length= 6 , max_length= 20, error_messages={ "min_length":"密码过短", "max_length":"密码过长", "required":"密码不允许为空" }) password2 = forms.CharField(min_length= 6 , max_length= 20, error_messages={ "min_length":"密码过短", "max_length":"密码过长", "required":"密码不允许为空" }) mobile = forms.CharField(min_length= 11 , max_length= 11, error_messages={ "min_length":"手机号过短", "max_length":"手机号过长", "required":"手机号不允许为空" }) # 使用全局钩子函数 , 检验两个密码是否一致 def clean(self): clean_data = super().clean() pw = clean_data.get('password') pw2 = clean_data.get('password2') if pw != pw2: raise forms.ValidationError('两次密码不一致') return clean_data
在注册视图中,实现获取用户提交的数据, 进行校验数据,保存数据
def post(self , request): # 获取用户提交的数据,将数据传递给 forms 组件进行校验 register_form = RegisterForm(request.POST) # 判断用户校验的数据是否合法 if register_form.is_valid(): return HttpResponse('注册成功') return HttpResponse('注册失败')
4、校验用户名重复
# 校验用户名重复 re_path('^username/(?P<username>[A-Za-z0-9_]{5,15})/count/$' , views.UsernameCountView.as_view()) class UsernameCountView(View): ''' 判断用户名是否重复 ''' def get(self , request , username): # 根据参数从数据库获取数据 count = User.objects.filter(username=username).count() return JsonResponse({'code':200 , 'errmsg':"OK" , 'count':count})
5、图片验证码
创建一个应用实现验证码的功能 —— verfications
1、配置缓冲验证码的 Redis 数据库
# 缓冲 验证码 "ver_code":{ "BACKEND" : "django_redis.cache.RedisCache", "LOCATION" : "redis://127.0.0.1:6379/2", "OPTIONS":{ "CLIENT_CLASS" : "django_redis.client.DefaultClient" } },
视图
# 响应图片验证码 re_path('^image_code/(?P<uuid>[\w-]+)/$' , views.ImageCodeView.as_view()) class ImageCodeView(View): ''' 响应图片验证码 ''' def get(self , request , uuid): # 调用生成图片验证码的功能 image , text = CodeImg.create_img() # 将验证码保存到数据库中 redis_conn = get_redis_connection('ver_code') redis_conn.setex('image_%s'%uuid , 400 , text) return HttpResponse(image , content_type='image/png')
修改前端页面中的对应标签内容
<li> <label>图形验证码:</label> <input type="text" name="image_code" id="pic_code" class="msg_input" v-model="image_code" @blur="check_image_code"> <img v-bind:src="image_code_url" alt="图形验证码" class="pic_code" @click="generate_image_code"> <span class="error_tip" v-show="error_image_code">请填写图形验证码</span> </li>
6、短信验证码
什么时候发送短信验证码 图片验证码校验成功之后发送
发送短信验证码的功能使用:https://console.yuntongxun.com/member/main
安装:pip install ronglian_sms_sdk
在应用下创建一个包实现发送短信的功能 —— ronglianyun , 创建模块: ccp_sms
from ronglian_sms_sdk import SmsSDK import json accId = '2c94811c88bf3503018900ca795012ba' accToken = '382b17b971884ddfad5c7ecadc07149b' appId = '2c94811c88bf3503018900ca7a9d12c1' # 单例模式 class CCP: _instance = None def __new__(cls, *args, **kwargs): # new 静态类方法,给对象分配内存空间 if cls._instance is None: # 如果类属性数据为 None , 说明当前类中没有实例化对象 # 给对象创建一个新的对象内存空间 cls._instance = super().__new__(cls, *args, **kwargs) return cls._instance def send_message(self,mobile, datas): sdk = SmsSDK(accId, accToken, appId) tid = '1' resp = sdk.sendMessage(tid, mobile, datas) resp = json.loads(resp) # 判断短信验证码是否发送成功 if resp["statusCode"] == "000000": # 短信验证码发送成功 return 0 else: return -1 send_code = CCP() <li> <label>短信验证码:</label> <input type="text" name="sms_code" id="msg_code" class="msg_input" v-model="sms_code" @blur="check_sms_code"> <a @click="send_sms_code" class="get_msg_code">获取短信验证码</a> <span class="error_tip" v-show="error_sms_code">请填写短信验证码</span> </li>
定义发送短信验证码的视图
# 发送短信验证码 re_path('^sms_code/(?P<mobile>1[3-9]\d{9})/$' , views.SmsCodeView.as_view()), class SmsCodeView(View): ''' 发送短信验证码 1、校验图片验证码 在接收发送短信验证码期间内,不允许重复的调用该视图 ''' def get(self , request , mobile): # 接收参数:uuid , 用户输入图片验证码 uuid = request.GET.get('uuid') image_code_client = request.GET.get('image_code') # 检验请求中的数据是否完整 if not all([uuid , image_code_client]): return HttpResponse("缺少不要的参数") # 校验图片验证码 # 从 Redis 数据库中获取该用户生成的图片验证码 redis_conn = get_redis_connection('ver_code') image_code_server = redis_conn.get('image_%s'%uuid) # 从数据库中获取手机号标记变量 sand_flag = redis_conn.get('sand_%s' % mobile) # 判断标记变量是否有值 if sand_flag: return JsonResponse({'code':RETCODE.THROTTLINGERR , 'errmsg':'发送短信验证码过于频繁'}) # 判断图片验证码是否在有效期内 if image_code_server is None: return JsonResponse({'code':RETCODE.IMAGECODEERR , 'errmsg':'图片验证码失效'}) # 将图片验证码删除 redis_conn.delete('image_%s'%uuid) # 判断图片验证码是否正确 image_code_server = image_code_server.decode() if image_code_client.lower() != image_code_server.lower(): return JsonResponse({'code': RETCODE.IMAGECODEERR, 'errmsg': '图片验证码输入错误'}) # 图片验证码正确 , 发送短信验证码 # 生成短信验证码 sms_code = '%05d'%random.randint(0,99999) # 保存短信验证码 redis_conn.setex('code_%s' % mobile, 400, sms_code) # 保存该手机的标记变量到数据库 redis_conn.setex('sand_%s' % mobile, 60, 1) # 发送短信验证码 send_code.send_message(mobile , (sms_code , 5)) return JsonResponse({'code':RETCODE.OK , 'errmsg':'短信验证码发送成功'}) 在项目中 ajax 响应的状态很多种。同一讲状态保存到一个文件包中,需要的时候进行调用。 在项目中创建一个共用包:utils 将 response_code 模块文本保存进去
7、完善注册视图
在 users 应用中的 forms 内对短信验证码的字段进行校验
sms_code = forms.CharField(max_length=5 , min_length=5) class RegisterView(View): ''' 用户注册 ''' def get(self , request): return render(request , 'register.html') def post(self , request): # 获取用户提交的数据,将数据传递给 forms 组件进行校验 register_form = RegisterForm(request.POST) # 判断用户校验的数据是否合法 if register_form.is_valid(): # 数据合法 username = register_form.cleaned_data.get('username') password = register_form.cleaned_data.get('password') mobile = register_form.cleaned_data.get('mobile') sms_code_client = register_form.cleaned_data.get('sms_code') # 从 redis 中获取生成短信验证码的 redis_conn = get_redis_connection('ver_code') sms_code_server = redis_conn.get('code_%s' % mobile) # 判断短信验证码是否有效 if sms_code_server is None: return render(request , 'register.html' , {'sms_code_errmsg':'短信验证码失效'}) if sms_code_client != sms_code_server.decode(): return render(request, 'register.html', {'sms_code_errmsg': '短信验证码输入错误'}) # 将数据保存到数据库中 user = User.objects.create_user(username=username , password=password , mobile=mobile) # 做状态保持 login(request , user) # 注册成功响应到登录页面 return redirect('login') else: # 用户数据不合法 # 从 forms 组件中获取数据异常信息 context = {'forms_errmsg':register_form.errors} return render(request ,'register.html' , context=context)
修改前端注册页面中对应的部分标签 , 获取后端的校验用户数据的异常信息
<li class="reg_sub"> <input type="submit" value="注 册"> <!-- 获取后端校验表单数据的异常信息 --> {% if forms_errmsg %} <span style="color: red">{{ forms_errmsg }}</span> {% endif %} </li> <li> <label>短信验证码:</label> <input type="text" name="sms_code" id="msg_code" class="msg_input" v-model="sms_code" @blur="check_sms_code"> <a @click="send_sms_code" class="get_msg_code">[[ sms_code_tip ]]</a> <span class="error_tip" v-show="error_sms_code">请填写短信验证码</span> <!-- 获取后端校验短信验证码的异常信息 --> {% if sms_code_errmsg %} <span style="color: red">{{ sms_code_errmsg }}</span> {% endif %} </li>
五、用户登录
1、响应登录页面的视图
# 用户登录 path('login/' , views.LoginView.as_view() , name='login'), class LoginView(View): ''' 用户登录视图 ''' def get(self , request): return render(request , 'login.html')
2、用户名登录
1、校验用户数据
class LoginForm(forms.Form): username = forms.CharField(min_length=5, max_length=15, error_messages={ "min_length": "用户名过短", "max_length": "用户名过长", "required": "用户名不允许为空" }) password = forms.CharField(min_length=6, max_length=20, error_messages={ "min_length": "密码过短", "max_length": "密码过长", "required": "密码不允许为空" }) remembered = forms.BooleanField(required=False) class LoginView(View): ''' 用户登录视图 ''' def get(self , request): return render(request , 'login.html') def post(self , request): login_form = LoginForm(request.POST) if login_form.is_valid(): username = login_form.cleaned_data.get('username') password = login_form.cleaned_data.get('password') remembered = login_form.cleaned_data.get('remembered') if not all([username , password]): return HttpResponse('缺少必要的参数') # 通过认证模块到数据库中进行获取用户数据 user = authenticate(username=username , password=password) # 判断在数据库中是否能够查询到用户数据 if user is None: return render(request , 'login.html' , {'account_errmsg':'用户名或者密码错误'}) # 状态保持 login(request , user) # 判断用户是否选择记住登录的状态 if remembered: # 用户选择记住登录状态 , 状态默认保存14天 request.session.set_expiry(None) else: # 用户状态不保存 , 关闭浏览器 , 数据销毁 request.session.set_expiry(0) # 响应首页 return redirect('index') else: context = {'forms_errors':login_form.errors} return render(request , 'login.html' , context=context)
3、手机号登录
Django 的 auth 认证系统中 默认是使用 用户名认证,使用其他数据进行认证 , 需要重新定义认证系统
1、实现一个类 , 这个类继承 ModelBackend 2、重写认证方法 authenticate
在 users 的应用下 创建一个文件 —— utils
from django.contrib.auth.backends import ModelBackend from users.models import User import re # 定义一个方法可以用手机号或者用户名查询数据的 def get_user(account): try: if re.match(r'1[3-9]\d{9}' , account): user = User.objects.get(mobile=account) else: user = User.objects.get(username=account) except Exception: return None else: return user class UsernameMobileBackend(ModelBackend): # 重写用户认证方法 def authenticate(self, request, username=None, password=None, **kwargs): # 调用查询用户数据的方法 user = get_user(username) # 判断密码是否正确 if user.check_password(password) and user: return user else: return None
修改 Django 项目中的配置文件的全局认证
# 配置自定义认证的方法 AUTHENTICATION_BACKENDS = ['users.utils.UsernameMobileBackend']
4、首页显示用户名
在后端的登录请求的视图中,在用户登录成功之后将用户名写到 Cookie 中。
def post(self , request): login_form = LoginForm(request.POST) if login_form.is_valid(): username = login_form.cleaned_data.get('username') password = login_form.cleaned_data.get('password') remembered = login_form.cleaned_data.get('remembered') if not all([username , password]): return HttpResponse('缺少必要的参数') # 通过认证模块到数据库中进行获取用户数据 user = authenticate(username=username , password=password) # 判断在数据库中是否能够查询到用户数据 if user is None: return render(request , 'login.html' , {'account_errmsg':'用户名或者密码错误'}) # 状态保持 login(request , user) # 判断用户是否选择记住登录的状态 if remembered: # 用户选择记住登录状态 , 状态默认保存14天 request.session.set_expiry(None) else: # 用户状态不保存 , 关闭浏览器 , 数据销毁 request.session.set_expiry(0) # 响应首页 response = redirect('index') # 将获取到的 用户名写入到 Cookie 中 response.set_cookie('username' , user.username , 3600) return response else: context = {'forms_errors':login_form.errors} return render(request , 'login.html' , context=context)
5、用户退出登录
退出登录:将用户的数据从 SESSION 会话中删除掉。
logout方法: 清除 session 会话的数据
<div class="login_btn fl"> 欢迎您:<em>[[ username ]]</em> <span>|</span> <a href="{% url 'logout' %}">退出</a> </div> # 退出登录 path('logout/' , views.LogoutView.as_view() , name='logout') class LogoutView(View): ''' 用户退出登录 ''' def get(self , request): # 清除用户保存的数据 logout(request) response = redirect('index') # 清除保存的 Cookie 数据 response.delete_cookie('username') return response
六、用户中心
1、响应用户中心
# 用户中心 path('info/' , views.UserInfoView.as_view() , name='info') class UserInfoView(View): ''' 用户中心 ''' def get(self , request): return render(request , 'user_center_info.html')
2、判断登录状态
使用 Django 用户认证提供 :LoginRequiredMixin 进行用户登录判断并且可以配置重定向到原来的请求页面 , 实现效果直接继承即可
class UserInfoView(LoginRequiredMixin , View): ''' 用户中心 ''' def get(self , request): return render(request , 'user_center_info.html')
到配置文件中重新定义认证登录重定向的 url
# 配置项目认证登录的重定向 LOGIN_URL = '/login/'
修改登录的视图
def post(self , request): login_form = LoginForm(request.POST) if login_form.is_valid(): username = login_form.cleaned_data.get('username') password = login_form.cleaned_data.get('password') remembered = login_form.cleaned_data.get('remembered') if not all([username , password]): return HttpResponse('缺少必要的参数') # 通过认证模块到数据库中进行获取用户数据 user = authenticate(username=username , password=password) # 判断在数据库中是否能够查询到用户数据 if user is None: return render(request , 'login.html' , {'account_errmsg':'用户名或者密码错误'}) # 状态保持 login(request , user) # 判断用户是否选择记住登录的状态 if remembered: # 用户选择记住登录状态 , 状态默认保存14天 request.session.set_expiry(None) else: # 用户状态不保存 , 关闭浏览器 , 数据销毁 request.session.set_expiry(0) next = request.GET.get('next') if next: # next 有值,重定向到指定的 url response = redirect(next) else: # 响应首页 response = redirect('index') # 将获取到的 用户名写入到 Cookie 中 response.set_cookie('username' , user.username , 3600) return response else: context = {'forms_errors':login_form.errors} return render(request , 'login.html' , context=context)
3、显示获取用户信息
class UserInfoView(LoginRequiredMixin , View): ''' 用户中心 ''' def get(self , request): # 从 request 中获取用户信息 context = { "username" : request.user.username, "mobile" : request.user.mobile, "email" : request.user.email, } return render(request , 'user_center_info.html' , context=context)
4、添加邮箱
1、在用户数据模型类中补充:邮箱验证状态的字段
# 邮箱验证字段 email_active = models.BooleanField(default=False)
验证登录的: LoginRequiredMixin 要求的返回值是 HttpResponse 对象 , 如果返回值的是 json 类型必须重写类方法
在项目全局的 utlis 包创建一个 view 模块
from django.contrib.auth.mixins import LoginRequiredMixin from django.http import JsonResponse from utils.response_code import RETCODE class LoginRequiredJSONMixin(LoginRequiredMixin): def handle_no_permission(self): # 让这个返回可以返回 json 类型对象即可 return JsonResponse({'code':RETCODE.SESSIONERR , 'errmsg':'用户未登录'}) class EmailView(LoginRequiredJSONMixin , View): ''' 用户添加邮箱 ''' def put(self , request): # put 请求的参数放在 request 的 body 中,并且一个字节传输的数据 # b'{'email':'123@com'}' json_str = request.body.decode() # '{'email':'123@com'}' # 使用 json 进行反序列化 json_dict = json.loads(json_str) email = json_dict.get('email') # 校验数据 if not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$' , email): return HttpResponseForbidden('邮箱参数有误') # 保存邮箱到数据库中 request.user.email = email request.user.save() # 添加成功 return JsonResponse({'code':RETCODE.OK , 'errmsg':'OK'})
5、验证邮箱
让 Django 发送邮件 , 是无法直接发送,需要借助SMTP服务器进行中转
需要到 Django 项目的配置文件中配置邮箱的需要的信息
# 发送邮件的配置参数 EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # 指定邮件后端 EMAIL_HOST = 'smtp.163.com' # 发邮件主机 EMAIL_PORT = 25 # 发邮件的端口 EMAIL_HOST_USER = '17841687578@163.com' # 授权邮箱 EMAIL_HOST_PASSWORD = 'BHPRRXBTMTCTGHVU' # 邮箱授权时获取的密码,非登录邮箱的密码 EMAIL_FROM = 'AC-<17841687578@163.com>' # 发件人抬头 # 设置邮箱的激活连接 EMAIL_VERIFY_URL = 'http://127.0.0.1:8000/verification/' 以网易云为例:在设置打开 SMTP/POP3 开启 IMAP/SMTP 和 POP3/SMTP 获取授权码
发送邮件
from django.core.mail import send_mail subject = '邮件验证' message = '阿宸真的超级帅' from_email = 'AC-<17841687578@163.com>' recipient_list = ['17841687578@163.com',] html_message = '<h1>阿宸真的超级帅</h1>' send_mail(subject, message, from_email, recipient_list , html_message=html_message) ''' subject: 邮件标题 message: 邮件正文(普通的文本文件,字符串) from_email: 发件人抬头 recipient_list: 收件人邮箱 (列表格式) html_message: 邮件正文(文件可以带渲染格式) '''
生成邮件激活连接 , 在 users 应用下的 utils 中操作
下载模块 itsdangerou==1.1.0 from itsdangerous import TimedJSONWebSignatureSerializer as TJWSS from ShopSystem import settings def generate_verify_email_url(user): ''' 生成邮箱激活连接 ''' # 调用加密的方法 s = TJWSS(settings.SECRET_KEY , 600) # 获取用户的基本数据 data = {'user_id':user.id , 'email':user.email} token = s.dumps(data) return settings.EMAIL_VERIFY_URL+'?token='+token.decode()
在保存邮箱的视图中 , 进行对邮箱发送一个验证连接
class EmailView(LoginRequiredJSONMixin , View): ''' 用户添加邮箱 ''' def put(self , request): # put 请求的参数放在 request 的 body 中,并且一个字节传输的数据 # b'{'email':'123@com'}' json_str = request.body.decode() # '{'email':'123@com'}' # 使用 json 进行反序列化 json_dict = json.loads(json_str) email = json_dict.get('email') # 校验数据 if not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$' , email): return HttpResponseForbidden('邮箱参数有误') # 保存邮箱到数据库中 request.user.email = email request.user.save() # 发送验证邮件 subject = 'AC商城邮箱验证' # 调用生成加密验证邮箱连接 veerify_url = generate_verify_email_url(request.user) html_message = f'<p>您的邮箱为:{email} , 请点击链接进行验证激活邮箱</p>' \ f'<p><a href="{veerify_url}">{veerify_url}</p>' send_mail(subject , '' , from_email=settings.EMAIL_FROM , recipient_list=[email],html_message=html_message) # 添加成功 return JsonResponse({'code':RETCODE.OK , 'errmsg':'OK'})
在 Django 中实现邮箱的数据验证,需要接收用户邮箱连接发送过来的参数
进行对参数解码,校验
def check_verify_email_token(token): ''' 校验邮箱连接中的参数 ''' s = TJWSS(settings.SECRET_KEY, 600) data = s.loads(token) # 获取解码好之后的参数 user_id = data.get('user_id') email = data.get('email') # 从数据库中查询是否有该用户 try: user = User.objects.get(id=user_id ,email=email) except Exception: return None else: return user
验证邮箱的视图
# 验证邮箱 path('verification/' , views.VerifyEmailView.as_view()), class VerifyEmailView(View): ''' 邮箱验证 ''' def get(self , request): token = request.GET.get('token') if not token: return HttpResponseForbidden('缺少必要参数') # 调用解码的方法 user = check_verify_email_token(token) if not user: return HttpResponseForbidden('用户不存在') # 判断用户优先是否已经验证 if user.email_active == 0: # 邮箱没有验证 user.email_active = 1 user.save() else: return HttpResponseForbidden('用户邮箱已验证') # 验证成功,重定向到用户中心 return redirect('info')
6、收货地址
class AddressView(View): ''' 用户收货地址 ''' def get(self , request): return render(request , 'user_center_site.html')
7、实现全国省市区名称数据
创建一个应用来操作实现地区数据 —— areas
设计地区数据模型类
from django.db import models # 自关联 # id name -id # 1 广东省 null # 2 湖北省 # 3 广州市 1 # 4 天河区 3 class Area(models.Model): name = models.CharField(max_length=20) # 自关联 : self # SET_NULL: 删除被关联的数据 , 对应链接的数据字段值会设置为 NULL parent = models.ForeignKey('self' , on_delete=models.SET_NULL , null=True,blank=True,related_name='subs') class Meta: db_table = 'areas'
在前端中使用 ajax 发送 url = /areas/ ; 获取地区数据 , 判断当请求路由中没有携带参数,则获取的是省份的数据
携带了 area_id=1,获取的是市或者区的数据
from django.shortcuts import render from django.views import View from areas.models import Area from utils.response_code import RETCODE from django.http import JsonResponse from django.core.cache import cache class AreasView(View): ''' 响应地区数据 ''' def get(self , request): area_id = request.GET.get('area_id') # 判断是否存在 area_id 参数 if not area_id: # 判断这个数据在内存中是否存在 province_list = cache.get('province_list') if not province_list: # 获取省份的数据 province_model_list = Area.objects.filter(parent_id__isnull=True) ''' 响应 json 数据 { 'code' : 200 'errmsg' : OK 'province_list':[ {id:110000 ; name:北京市}, {id:120000 ; name:天津市}, …… ] } ''' province_list = [] for province_model in province_model_list: province_dict = { "id" : province_model.id, "name" : province_model.name, } province_list.append(province_dict) # 将数据缓存到内存中 cache.set('province_list',province_list , 3600) return JsonResponse({'code':RETCODE.OK , 'errmsg':"OK" , 'province_list':province_list}) else: # 获取 市 或者 区 的数据 ''' { 'code' : 200 'errmsg' : OK sub_data : { id : 省110000 name : 广东省 subs : [ {id , name}, {id , name}, {id , name}, …… ] } } ''' sub_data = cache.get('sub_data_%s'%area_id) if not sub_data: parent_model = Area.objects.get(id=area_id) # 获取关联 area_id 的对象数据 sub_model_list = parent_model.subs.all() subs = [] for sub_model in sub_model_list: sub_dict = { 'id' : sub_model.id, 'name' : sub_model.name, } subs.append(sub_dict) sub_data = { 'id' : parent_model.id, 'name' : parent_model.name, 'subs' : subs } cache.set('sub_data_%s'%area_id , sub_data , 3600) return JsonResponse({'code':RETCODE.OK , 'errmsg':"OK" , 'sub_data':sub_data})
修改前端 user_center_site.html 中对应标签的内容
<div class="form_group"> <label>*所在地区:</label> <select v-model="form_address.province_id"> <option value="0">请选择</option> <option :value="province.id" v-for="province in provinces">[[ province.name ]]</option> </select> <select v-model="form_address.city_id"> <option value="0">请选择</option> <option :value="city.id" v-for="city in cities">[[ city.name ]]</option> </select> <select v-model="form_address.district_id"> <option value="0">请选择</option> <option :value="district.id" v-for="district in districts">[[ district.name ]]</option> </select> </div>
8、创建收货地址模型类
让其他模型类可以共用时间的字段,在项目中的 utils 包内创建一个 model 文件
from django.db import models class BaseModel(models.Model): # 创建时间 create_time = models.DateTimeField(auto_now_add=True) # 更新时间 update_time = models.DateTimeField(auto_now=True) class Meta: # 在迁移数据库的时候不为该模型类单独创建一张表 abstract = True
需要使用时间的模型类继承上面该类即可。
创建用户收货地址模型类
class Address(BaseModel): # 用户收货地址 # 关联用户 user = models.ForeignKey(User , on_delete=models.CASCADE , related_name='address') receiver = models.CharField(max_length=20) province = models.ForeignKey('areas.Area' , on_delete=models.PROTECT , related_name='province_address') city = models.ForeignKey('areas.Area' , on_delete=models.PROTECT , related_name='city_address') district = models.ForeignKey('areas.Area' , on_delete=models.PROTECT , related_name='district_address') palce = models.CharField(max_length=50) mobile = models.CharField(max_length=11) tel = models.CharField(max_length=20 , null=True , blank=True , default='') email = models.CharField(max_length=20 , null=True , blank=True , default='') is_delete = models.BooleanField(default=False) class Meta: db_table = 'address'
在用户个人数据模型类中保存默认收货地址 , 默认地址只有一个
default_address = models.ForeignKey('Address' , on_delete=models.SET_NULL , null=True, blank=True , related_name='users')
9、修改密码
# 修改密码 path('changepwd/' , views.ChangePasswordView.as_view(), name='changepwd'), class ChangePasswordView(View): ''' 用户修改密码 ''' def get(self , request): return render(request,'user_center_pass.html') def post(self , request): # 接收用户输入的密码 old_password = request.POST.get('old_password') new_password = request.POST.get('new_password') new_password2 = request.POST.get('new_password2') # 校验数据 , 数据是否完整 if not all([old_password , new_password , new_password2]): return HttpResponseForbidden('缺少必要的数据') # 校验旧密码是否正确 if not request.user.check_password(old_password): return render(request , 'user_center_pass.html' , {'origin_password_errmsg':'旧密码不正确'}) # 校验新密码中的数据是否合法 if not re.match(r'^[0-9A-Za-z]{6,20}$' , new_password): return render(request, 'user_center_pass.html', {'change_password_errmsg': '密码格式不正确'}) # 校验两次新密码是否一致 if new_password != new_password2: return render(request, 'user_center_pass.html', {'change_password_errmsg': '两次密码不一致'}) # 密码数据正确合法,将新的密码重新保存 request.user.set_password(new_password) request.user.save() # 跟新状态保持 , 清理原有的密码数据 logout(request) response = redirect('login') response.delete_cookie('username') return response
10、新增收货地址
# 新增收货地址 path('addresses/create/', views.AddressCreateView.as_view()), class AddressCreateView(View): ''' 用户新增收货地址 ''' def post(self , request): json_str = request.body.decode() json_dict = json.loads(json_str) receiver = json_dict.get('receiver') province_id = json_dict.get('province_id') city_id = json_dict.get('city_id') district_id = json_dict.get('district_id') place = json_dict.get('place') mobile = json_dict.get('mobile') tel = json_dict.get('tel') email = json_dict.get('email') # 校验数据 , 数据完整性 if not all([receiver , province_id , city_id , district_id , place , mobile]): return HttpResponseForbidden('缺少不要数据') if not re.match(r'^1[3-9]\d{9}$' , mobile): return HttpResponseForbidden('手机号输入有误') if tel: if not re.match(r'^(0[0-9]{2,3}-)?([2-9][0-9]{6,7})+(-[0-9]{1,4})?$', tel): return HttpResponseForbidden('固定电话输入有误') if email: if not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email): return HttpResponseForbidden('邮箱输入有误') # 将数据保存到数据库中 address = Address.objects.create( user=request.user, receiver = receiver, province_id = province_id, city_id = city_id, district_id = district_id, palce = place, mobile = mobile, tel = tel, email = email, ) address_dict = { 'id' : address.id, 'receiver': address.receiver, 'province': address.province.name, 'city': address.city.name, 'district': address.district.name, 'place': address.palce, 'mobile': address.mobile, 'tel': address.tel, 'email': address.email, } return JsonResponse({'code':RETCODE.OK , 'errmsg':'新增地址成功' , 'address':address_dict})
11、渲染收货地址
class AddressView(View): ''' 用户收货地址 ''' def get(self , request): # 获取当前登录的用户信息 login_user = request.user # 根据当前登录的用户信息,获取对应的地址数据 addresses = Address.objects.filter(user=login_user , is_delete=False) address_list = [] for address in addresses: address_dict = { 'id': address.id, 'receiver': address.receiver, 'province': address.province.name, 'city': address.city.name, 'district': address.district.name, 'place': address.palce, 'mobile': address.mobile, 'tel': address.tel, 'email': address.email, } address_list.append(address_dict) context = { 'addresses' : address_list, # 获取用户的默认收货地址 'default_address_id' : login_user.default_address_id, # 计算用户的收货地址个数 'count':addresses.count() } return render(request , 'user_center_site.html' , context=context)
修改前端对应标签:user_center_site.html
<div class="right_content clearfix" v-cloak> <div class="site_top_con"> <a @click="show_add_site">新增收货地址</a> <span>你已创建了<b>{{ count }}</b>个收货地址,最多可创建<b>20</b>个</span> </div> <div class="site_con" v-for="(address , index) in addresses" :key="address.id"> <div class="site_title"> <h3>[[ address.receiver ]]</h3> <a @click="show_edit_title(index)" class="edit_icon"></a> <em v-if="address.id === default_address_id">默认地址</em> <span class="del_site" @click="delete_address(index)">×</span> </div> <ul class="site_list"> <li><span>收货人:</span><b>[[ address.receiver ]]</b></li> <li><span>所在地区:</span><b>[[ address.province ]] [[ address.city ]] [[ address.district ]]</b></li> <li><span>地址:</span><b>[[ address.place ]]</b></li> <li><span>手机:</span><b>[[ address.mobile ]]</b></li> <li><span>固定电话:</span><b>[[ address.tel ]]</b></li> <li><span>电子邮箱:</span><b>[[ address.email ]]</b></li> </ul> <div class="down_btn"> <a v-if="address.id != default_address_id" @click="set_default(index)">设置默认地址</a> <a class="edit_icon" @click="show_edit_site(index)" >编辑</a> </div> </div> </div>
12、修改\删除收货地址
# 修改、删除收货地址 re_path('^addresses/(?P<address_id>\d+)/$', views.UpdateAddressView.as_view()), class UpdateAddressView(View): ''' 修改/删除收货地址 ''' def put(self , request , address_id): # 用户修改收货地址 json_str = request.body.decode() json_dict = json.loads(json_str) receiver = json_dict.get('receiver') province_id = json_dict.get('province_id') city_id = json_dict.get('city_id') district_id = json_dict.get('district_id') place = json_dict.get('place') mobile = json_dict.get('mobile') tel = json_dict.get('tel') email = json_dict.get('email') # 校验数据 , 数据完整性 if not all([receiver, province_id, city_id, district_id, place, mobile]): return HttpResponseForbidden('缺少不要数据') if not re.match(r'^1[3-9]\d{9}$', mobile): return HttpResponseForbidden('手机号输入有误') if tel: if not re.match(r'^(0[0-9]{2,3}-)?([2-9][0-9]{6,7})+(-[0-9]{1,4})?$', tel): return HttpResponseForbidden('固定电话输入有误') if email: if not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email): return HttpResponseForbidden('邮箱输入有误') Address.objects.filter(id=address_id).update( user=request.user, receiver=receiver, province_id=province_id, city_id=city_id, district_id=district_id, palce=place, mobile=mobile, tel=tel, email=email, ) address = Address.objects.get(id=address_id) address_dict = { 'id': address.id, 'receiver': address.receiver, 'province': address.province.name, 'city': address.city.name, 'district': address.district.name, 'place': address.palce, 'mobile': address.mobile, 'tel': address.tel, 'email': address.email, } return JsonResponse({'code': RETCODE.OK, 'errmsg': '修改地址成功', 'address': address_dict}) def delete(self , request , address_id): # 删除地址 address = Address.objects.get(id=address_id) address.is_delete = True address.save() return JsonResponse({'code': RETCODE.OK, 'errmsg': '删除地址成功'})
13、设置默认收货地址
# 设置用户默认收货地址 re_path('^addresses/(?P<address_id>\d+)/default/$', views.DefaultAddressView.as_view()), class DefaultAddressView(View): ''' 设置默认收货地址 ''' def put(self , request , address_id): # 获取默认地址的数据对象 address = Address.objects.get(id=address_id) request.user.default_address = address request.user.save() return JsonResponse({'code': RETCODE.OK, 'errmsg': '默认收货地址设置成功'})
七、商品数据
SPU:标准产品单位 , 表示一组类似属性或者特征的商品集合。【商品的基本信息和属性:名称,描述,品牌】 , 对商品的基本定义 SKU:库存单位,表示具体的商品或者库存【商品的规格 , 价格,尺码,版本】
创建一个应用来操作商品 相关数据 —— goods
迁移数据库 , 执行 goods_data.sql 插入商品数据
1、首页的商品分类
{ 1:{ channels:[ {id:1 , name:手机 , url:}, {} {} ], sub_cats:[ {id:500 name:手机通讯, sub_cat:[ {id:520 , name:华为}, {},…… ]}, {}…… ] }, 2:{}, 3:{}, …… } class IndexView(View): ''' 响应首页 ''' def get(self , request): # 定义一个空的字典 , 存放商品频道分类的数据 categories = {} # 查询商品分组频道的所有数据 channels = GoodsChannel.objects.all() # 获取到所有的商品频道组 for channel in channels: # 获取商品频道组的 id ,作为分组的 key group_id = channel.group_id # 判断获取到的分组 id 在字典中是否存在 , 存在则不添加 if group_id not in categories: categories[group_id] = {'channels':[] , 'sub_cats':[]} # 查询一级商品的数据信息 # 根据商品的外键数据 , 判断是否为一级商品类别 cat1 = channel.category categories[group_id]['channels'].append( { 'id':cat1.id, 'name':cat1.name, 'url': channel.url } ) # 获取二级的商品类别数据 # 二级的数据根据一级的类别 id 进行获取:cat1.subs.all() for cat2 in cat1.subs.all(): cat2.sub_cats = [] categories[group_id]['sub_cats'].append( { 'id':cat2.id, 'name':cat2.name, 'sub_cat':cat2.sub_cats } ) # 获取三级的数据 for cat3 in cat2.subs.all(): cat2.sub_cats.append( { 'id':cat3.id, 'name':cat3.name } ) # 首页商品推荐广告数据 # 获取所有的推荐商品广告类别 content_categories = ContentCategory.objects.all() contents = {} for content_category in content_categories: contents[content_category.key] = Content.objects.filter( category_id=content_category.id, status=True ).all().order_by('sequence') context = {'categories':categories , 'contents':contents} print(contents) return render(request , 'index.html' , context=context)
修改index.html页面中对应的标签内容
<ul class="slide"> {% for content in contents.index_lbt %} <li><a href="{{ content.url }}"><img src="/static/images/goods/{{ content.image }}.jpg" alt="幻灯片"></a></li> {% endfor %} </ul> <div class="news"> <div class="news_title"> <h3>快讯</h3> <a href="#">更多 ></a> </div> <ul class="news_list"> {% for content in contents.index_kx %} <li><a href="{{ content.url }}">{{ content.title }}</a></li> {% endfor %} </ul> {% for content in contents.index_ytgg %} <a href="{{ content.url }}" class="advs"><img src="/static/images/goods/{{ content.image }}.jpg"></a> {% endfor %} </div>
2、商品列表页
因为商品列表中页需要商品分类的功能 , 将首页中的商品分类功能进行抽取出来单独定义一个模块中,需要导入调用。
在 contents 应用中。创建 utils 模块 , 实现商品分类模块功能。
制作列表页中列表导航栏(面包屑) , 在应用中创建 utils 模块 , 列表导航栏(面包屑) 。
from goods.models import GoodsCategory def get_breadcrumb(category): # 一级:breadcrumb = {cat1:''} # 二级:breadcrumb = {cat1:'',cat2:''} # 三级:breadcrumb = {cat1:'',cat2:'',cat3:''} breadcrumb = {'cat1':'','cat2':'','cat3':''} if category.parent == None: # 没有外键数据 , 说明类别属于一级 breadcrumb['cat1'] = category elif GoodsCategory.objects.filter(parent_id = category.id).count() == 0: # 判断是否有外键被链接对象 , 如果没有说明这个是三级的数据 # 三级是通过二级间接连接到一级的数据 , 无法直接拿到一级的名称 cat2 = category.parent breadcrumb['cat1'] = cat2.parent breadcrumb['cat2'] = cat2 breadcrumb['cat3'] = category else: # 二级 breadcrumb['cat1'] = category.parent breadcrumb['cat2'] = category return breadcrumb class GoodsListView(View): ''' 商品列表页 ''' def get(self ,request , category_id , pag_num): categories = get_categories() # 获取到当前列表的商品类别对象 category = GoodsCategory.objects.get(id=category_id) # 调用生成面包屑的功能 breadcrumb = get_breadcrumb(category) # 商品排序 # 获取请求的参数:sort , 进行判断商品排序的方式 # 如果没有 sort 参数 , 则按照默认排序 sort = request.GET.get('sort' , 'default') if sort == 'price': sort_field = 'price' elif sort == 'hot': sort_field = 'sales' else: sort = 'default' sort_field = 'create_time' skus = SKU.objects.filter(is_launched=True , category_id=category_id).order_by(sort_field) # 对商品进行分页 paginator = Paginator(skus , 5) # 获取当前页面的数据 page_skus = paginator.page(pag_num) # 获取分页的总数 total_num = paginator.num_pages context = { 'categories':categories, 'breadcrumb':breadcrumb, 'page_skus':page_skus, 'sort':sort, 'pag_num':pag_num, 'category_id':category_id, 'total_num':total_num } return render(request , 'list.html' , context=context)
修改前端list.html对应的标签内容
<div class="breadcrumb"> <a href="http://shouji.jd.com/">{{ breadcrumb.cat1.name }}</a> <span>></span> <a href="javascript:;">{{ breadcrumb.cat2.name }}</a> <span>></span> <a href="javascript:;">{{ breadcrumb.cat3.name }}</a> </div> <div class="r_wrap fr clearfix"> <div class="sort_bar"> <a href="{% url 'list' category_id pag_num %}?sort=default" {% if sort == 'default' %} class="active"{% endif %}>默认</a> <a href="{% url 'list' category_id pag_num %}?sort=price" {% if sort == 'price' %} class="active"{% endif %}>价格</a> <a href="{% url 'list' category_id pag_num %}?sort=hot" {% if sort == 'hot' %} class="active"{% endif %}>人气</a> </div> <ul class="goods_type_list clearfix"> {% for sku in page_skus %} <li> <a href="detail.html"><img src="/static/images/goods{{ sku.default_image.url }}.jpg"></a> <h4><a href="detail.html">{{ sku.name }}</a></h4> <div class="operate"> <span class="price">¥{{ sku.price }}</span> <span class="unit">台</span> <a href="#" class="add_goods" title="加入购物车"></a> </div> </li> {% endfor %} </ul> </div> <script> $(function () { $('#pagination').pagination({ currentPage: {{ pag_num }}, totalPage: {{ total_num }}, callback:function (current) { location.href = '/list/{{ category_id }}/' + current + '/?sort={{ sort }}'; } }) }); </script>
在页面的底部
<!-- 分页器盒子 --> <div class="pagenation"> <div id="pagination" class="page"></div> </div>
3、热销商品排行
# 热销商品排行 re_path('^hot/(?P<category_id>\d+)/$' , views.HotGoodsView.as_view()), class HotGoodsView(View): ''' 热销商品排行 ''' def get(self , request , category_id): # 获取该类别商品的数据 skus = SKU.objects.filter(is_launched=True, category_id=category_id).order_by('-sales')[:2] hot_skus = [] for sku in skus: sku_dict = { 'id': sku.id, 'name': sku.name, 'price':sku.price, 'default_image_url': settings.STATIC_URL+'images/goods/'+sku.default_image.url+'.jpg' } hot_skus.append(sku_dict) return JsonResponse({'code':RETCODE.OK , 'errmsg':'OK' , 'hot_skus':hot_skus})
修改前端 list.html 页面中对应的标签内容
<script type="text/javascript"> let category_id = {{ category_id }}; </script> <div class="new_goods"> <h3>热销排行</h3> <ul> <li v-for="sku in hot_skus" :key="sku.id"> <a href="detail.html"><img :src="sku.default_image_url"></a> <h4><a href="detail.html">[[ sku.name ]]</a></h4> <div class="price">¥[[ sku.price ]]</div> </li> </ul> </div>
4、商品详情页
# 商品详情页 # 这个是哪一个商品:sku.id re_path('^detail/(?P<sku_id>\d+)/$', views.DetailGoodsView.as_view() ,name='detail'), class DetailGoodsView(View): ''' 商品详情页 ''' def get(self , request , sku_id): categories = get_categories() sku = SKU.objects.get(id=sku_id) breadcrumb = get_breadcrumb(sku.category) # 通过 sku.id 获取商品对象的对应规格信息的选项 sku_specs = SKUSpecification.objects.filter(sku_id=sku_id).order_by('spec_id') # 创建一个空的列表 用来存储当前 sku 对应的规格选项数据 sku_key = [] # 遍历当前 sku 的规格选项 for spec in sku_specs: # 将每个规格选项的 ID 添加到 sku_key 列表中 sku_key.append(spec.option.id) # [8, 11] 颜色:金色 ,内存:64GB # [1, 4, 7] 屏幕尺寸:13.3英寸 颜色:银色 ,内存:core i5/8G内存/512G存储 # 获取当前商品的所有 sku # 保证选择不同的规格的情况下 , 商品不变 spu_id = sku.spu_id skus = SKU.objects.filter(spu_id=spu_id) # 构建商品的不同规格参数,sku的选项字段 spec_sku_map = {} for i in skus: # 获取sku规格的参数 s_pecs = i.specs.order_by('spec_id') # 创建一个空列表 , 用于存储 sku 规格参数 key = [] # 遍历当前 sku 规格参数列表 for spec in s_pecs: key.append(spec.option.id) spec_sku_map[tuple(key)] = i.id # 获取当前商品的规格名称 # 根据商品的 ID 获取当前商品的所有规格名称 goods_specs = SPUSpecification.objects.filter(spu_id=spu_id).order_by('id') # 前端渲染 # 实现根据规格选项生成对应 sku.id, 更新规格对象个规格选项信息。 # 为了给用户展示所有的规格参数 for index , spec in enumerate(goods_specs): # 复制 sku_key 列表中的数据,避免直接 sku_key 列表中的内容 key = sku_key[:] # 获取当前 specs 对象的规格名称 spec_options = spec.options.all() # 遍历当前商品的规格名称 for spec_option in spec_options: # 将当前规格选项对象 spec_option 的 id 赋值给 key列表中 index 的位置,用于查询对应 sku 参数内容 key[index] = spec_option.id # 根据列表中的值, 在 spec_sku_map 字典中查询对应的 sku 数据 spec_option.sku_id = spec_sku_map.get(tuple(key)) # 更新每个规格对象的选项内容 spec.spec_options = spec_options context = { 'categories' : categories, 'breadcrumb' : breadcrumb, 'sku':sku, 'specs' : goods_specs } return render(request , 'detail.html' , context=context) <script type="text/javascript"> let category_id = {{ sku.category_id }}; let sku_price = {{ sku.price }}; let sku_id = {{ sku.id }}; </script>
5、统计分类商品的访问量
# 统计分类商品访问量 re_path('^detail/visit/(?P<category_id>\d+)/$' , views.DetailVisitView.as_view()), class DetailVisitView(View): ''' 分类商品访问量 ''' def post(self , request , category_id): # 校验数据 try: category = GoodsCategory.objects.get(id=category_id) except Exception: return HttpResponseForbidden('商品参数类别不存在') t = timezone.localtime() # yyyy-mm-dd , 格式化当前时间 today = '%d-%02d-%02d'%(t.year , t.month , t.day) try: # 获取当前类别在数据库是否存在 , 如果修改时间以及访问量即可 count_data = GoodsVisitCount.objects.get(category=category_id , date=today) except GoodsVisitCount.DoesNotExist: # DoesNotExist: 模型类数据不存在 # 创建一个空的数据对象 count_data = GoodsVisitCount() count_data.category = category count_data.date = today count_data.count += 1 count_data.save() return JsonResponse({'code':RETCODE.OK , 'errmsg':"OK"})
6、商品搜索
全文搜索引擎
需要下载这两个框架 django_haystack whoosh
可以对表中的某些字段进行关键字分析 , 建立关键词对应的索引数据
在配置文件中 INSTALLED_APPS 的列表中添加: haystack;
在配置文件末尾添加
# 配置 haystack HAYSTACK_CONNECTIONS = { 'default': { # 设置搜索引擎 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', 'PATH':os.path.join(BASE_DIR,'whoosh_index'), }, } # 当数据库改变时,自动更新新引擎 HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
在 goods 应用中创建 search_indexes.py 文件
from haystack import indexes from goods.models import SKU # 类名必须为模型类名+Index class SKUIndex(indexes.SearchIndex, indexes.Indexable): # document=True 代表搜索引擎将使用此字段的内容作为引擎进行检索 # use_template=True 代表使用索引模板建立索引文件 text = indexes.CharField(document=True, use_template=True) # 将索引类与模型类进行绑定 def get_model(self): return SKU # 设置索引的查询范围 def index_queryset(self, using=None): return self.get_model().objects.all()
在templates 目录下创建搜索引擎文件的
templates |---- search |---- indexes |---- goods(这个目录的名称是指定搜索模型类所在的应用名称) |---- sku_text.txt (这个名称根据小写模型类名称_text.txt)
在 sku_text.txt 中配置搜索索引字段
# 指定根据表中的字段建立索引 {{ object.name }} {{ object.caption }}
在终端执行创建全文搜索索引文件
python manage.py rebuild_index
实现分页,在 goods 应用的视图下
from haystack.query import SearchQuerySet def search_view(request): query = request.GET.get('q', '') page = request.GET.get('page', 1) # 使用 Haystack 的 SearchQuerySet 进行搜索,过滤出包含搜索关键词的结果集 search_results = SearchQuerySet().filter(content=query) paginator = Paginator(search_results, 6) # 每页显示10条搜索结果 try: results = paginator.page(page) except PageNotAnInteger: # 处理用户在 URL 中输入的页数不是整数的情况,将当前页设为第一页 results = paginator.page(1) except EmptyPage: # 处理用户请求的页面超出搜索结果范围的情况,将当前页设为最后一页。 results = paginator.page(paginator.num_pages) return render(request, 'search.html', {'results': results, 'query': query})
在应用下配置url
path('search/' , views.search_view , name='search') <div class=" clearfix"> <ul class="goods_type_list clearfix"> {% for result in results %} <li> {# object取得才是sku对象 #} <a href="/detail/{{ result.object.id }}/"><img src="/static/images/goods/{{ result.object.default_image.url }}.jpg"></a> <h4><a href="/detail/{{ result.object.id }}/">{{ result.object.name }}</a></h4> <div class="operate"> <span class="price">¥{{ result.object.price }}</span> <span>{{ result.object.comments }}评价</span> </div> </li> {% empty %} <p>没有找到您要查询的商品。</p> {% endfor %} </ul> <div class="pagination"> {% if results.has_previous %} <a href="?q={{ query }}&page=1">« 首页</a> <a href="?q={{ query }}&page={{ results.previous_page_number }}">上一页</a> {% endif %} 当前页: {{ results.number }} of 总页数: {{ results.paginator.num_pages }} {% if results.has_next %} <a href="?q={{ query }}&page={{ results.next_page_number }}">下一页</a> <a href="?q={{ query }}&page={{ results.paginator.num_pages }}">尾页 »</a> {% endif %} </div> </div>
八、购物车
1、响应添加购物车数据
创建应用实现购物车的功能操作 —— carts
配置 redis 数据库缓存购物车中的商品数据
# 缓存 购物车商品id "carts":{ "BACKEND" : "django_redis.cache.RedisCache", "LOCATION" : "redis://127.0.0.1:6379/3", "OPTIONS":{ "CLIENT_CLASS" : "django_redis.client.DefaultClient" } }, # 购物车页面 path('carts/' , views.CartsView.as_view() , name='carts'), class CartsView(View): ''' 响应购物车视图 ''' def get(self , request): # 响应购物车页面 return render(request , 'cart.html') def post(self , request): # 商品添加购物车 json_str = request.body.decode() json_dict = json.loads(json_str) sku_id = json_dict.get('sku_id') count = json_dict.get('count') selected = json_dict.get('selected' , True) try: SKU.objects.get(id=sku_id) except Exception: return HttpResponseForbidden('sku_id 商品数据不存在') user = request.user redis_conn = get_redis_connection('carts') # user_id = {sku_id:count} redis_conn.hincrby('cart_%s'%user.id , sku_id , count) if selected: # 结果为 True , 勾选的进行保存 redis_conn.sadd('selected_%s'%user.id , sku_id) return JsonResponse({'code': RETCODE.OK, 'errmsg':'OK'})
2、响应渲染购物车页面
class CartsView(View): ''' 响应购物车视图 ''' def get(self , request): # 响应购物车页面 user = request.user redis_conn = get_redis_connection('carts') redis_cart = redis_conn.hgetall('cart_%s' % user.id) redis_selected = redis_conn.smembers('selected_%s'%user.id) # cart_dict = {sku1:{count:200 , selected:true},{}……} cart_dict = {} for sku_id , count in redis_cart.items(): cart_dict[int(sku_id)] = { 'count': int(count), 'selected' : sku_id in redis_selected } # 获取购物车所有的商品数据 sku_ids = cart_dict.keys() skus = SKU.objects.filter(id__in=sku_ids) cart_skus = [] for sku in skus: cart_skus_dict = { 'id' : sku.id, 'name':sku.name, 'price' : str(sku.price), 'count' : cart_dict.get(sku.id).get('count'), 'selected' : str(cart_dict.get(sku.id).get('selected')), 'amount':str(sku.price * cart_dict.get(sku.id).get('count')), 'default_image_url':settings.STATIC_URL+'images/goods/'+sku.default_image.url+'.jpg' } cart_skus.append(cart_skus_dict) context = {'cart_skus':cart_skus} return render(request , 'cart.html' , context=context) <script type="text/javascript"> let carts = {{ cart_skus|safe }}; </script>
3、修改购物车商品数据
def put(self , request): # 修改购物车商品数据 # {sku_id: 1, count: 2, selected: true} json_str = request.body.decode() json_dict = json.loads(json_str) sku_id = json_dict.get('sku_id') count = json_dict.get('count') selected = json_dict.get('selected', True) try: sku = SKU.objects.get(id=sku_id) except Exception: return HttpResponseForbidden('sku_id 商品数据不存在') user = request.user redis_conn = get_redis_connection('carts') redis_conn.hincrby('cart_%s' % user.id, sku_id, count) if selected: redis_conn.sadd('selected_%s' % user.id, sku_id) else: redis_conn.srem('selected_%s' % user.id, sku_id) cart_skus_dict = { 'id': sku.id, 'name': sku.name, 'price': sku.price, 'count': count, 'selected': selected, 'amount': sku.price * count, 'default_image_url': settings.STATIC_URL + 'images/goods/' + sku.default_image.url + '.jpg' } return JsonResponse({'code':RETCODE.OK , 'errmsg':"OK" , 'cart_sku':cart_skus_dict})
4、删除购物车商品
def delete(self , request): # 删除购物车商品 json_str = request.body.decode() json_dict = json.loads(json_str) sku_id = json_dict.get('sku_id') user = request.user redis_conn = get_redis_connection('carts') redis_conn.hdel('cart_%s' % user.id, sku_id) redis_conn.srem('selected_%s' % user.id, sku_id) return JsonResponse({'code': RETCODE.OK, 'errmsg': "OK"})
5、全选购物车商品
# 全选购物车 path('carts/selection/' , views.CratSelectAllView.as_view()), class CratSelectAllView(View): ''' 全选购物车商品 ''' def put(self , request): json_dict = json.loads(request.body.decode()) selected = json_dict.get('selected') user = request.user redis_conn = get_redis_connection('carts') redis_cart = redis_conn.hgetall('cart_%s' % user.id) redis_sku_id = redis_cart.keys() if selected: redis_conn.sadd('selected_%s' % user.id, *redis_sku_id) else: for sku_id in redis_sku_id: redis_conn.srem('selected_%s' % user.id, sku_id) return JsonResponse({'code': RETCODE.OK, 'errmsg': "OK"})
5、浏览记录
用户浏览记录临时数据 , 数据读写频繁 , 存放在 redis
# 缓存 浏览记录商品 id "history":{ "BACKEND" : "django_redis.cache.RedisCache", "LOCATION" : "redis://127.0.0.1:6379/4", "OPTIONS":{ "CLIENT_CLASS" : "django_redis.client.DefaultClient" } }, # 用户浏览记录 path('browse_histories/' , views.UserBrowerHistoryView.as_view()), class UserBrowerHistoryView(View): def get(self , request): redis_conn = get_redis_connection('history') user = request.user sku_ids = redis_conn.lrange('history_%s'%user.id , 0 , -1) skus = [] for sku_id in sku_ids: sku = SKU.objects.get(id = sku_id) sku_dict = { 'id':sku.id, 'name': sku.name, 'price' : sku.price, 'default_image_url':settings.STATIC_URL+'images/goods/'+sku.default_image.url+'.jpg' } skus.append(sku_dict) return JsonResponse({'code':RETCODE.OK , 'errmsg':"OK" , 'skus':skus}) def post(self , request): json_dict = json.loads(request.body.decode()) sku_id = json_dict.get('sku_id') try: SKU.objects.get(id=sku_id) except Exception: return HttpResponseForbidden('sku_id 商品数据不存在') redis_conn = get_redis_connection('history') user = request.user # 去重 redis_conn.lrem('history_%s'%user.id , 0 , sku_id) redis_conn.lpush('history_%s'%user.id , sku_id) # 截取 redis_conn.ltrim('history_%s'%user.id , 0 , 20) return JsonResponse({'code':RETCODE.OK , 'errmsg':"OK"})
九、订单
1、响应订单
创建应用实现订单的功能 —— orders
# 订单页面 path('settlement/' , views.OrderSettlementView.as_view() , name='settlement'), class OrderSettlementView(View): ''' 购物车结算订单页面 ''' def get(self , request): user = request.user # 获取用户收货地址 try: addresses = Address.objects.filter(is_delete=False , user=user) except Exception: addresses = None # 获取购物车商品数据 redis_conn = get_redis_connection('carts') redis_cart = redis_conn.hgetall('cart_%s'%user.id) redis_selected = redis_conn.smembers('selected_%s'%user.id) # 获取勾选中的商品 id new_cart_dict = {} for sku_id in redis_selected: new_cart_dict[int(sku_id)] = int(redis_cart[sku_id]) sku_ids = new_cart_dict.keys() skus = SKU.objects.filter(id__in=sku_ids) # 总件数 , 金额 total_number = 0 total_amount = 0 for sku in skus: sku.count = new_cart_dict[sku.id] sku.amount = sku.price * sku.count total_number += sku.count total_amount += sku.amount freight = 35 context = { 'addresses': addresses, 'skus' : skus, 'total_amount' : total_amount, 'total_number' : total_number, 'freight' : freight, 'payment_amount':total_amount + freight } return render(request , 'place_order.html' , context=context) <script type="text/javascript"> let default_address_id = {{ user.default_address.id }}; let payment_amount = {{ payment_amount }}; </script>
2、提交订单
# 提交订单 path('orders/commit/' , views.OrderCommitView.as_view()) class OrderCommitView(View): ''' 提交订单 ''' def post(self , request): json_dict = json.loads(request.body.decode()) address_id = json_dict.get('address_id') pay_method = json_dict.get('pay_method') try: address = Address.objects.get(id=address_id) except Exception: return HttpResponseForbidden('用户收货地址数据不存在') if pay_method not in [OrderInfo.PAY_METHODS_ENUM['CASH'] , OrderInfo.PAY_METHODS_ENUM['ALIPAY']]: return HttpResponseForbidden('支付方式不正确') user = request.user order_id = timezone.localdate().strftime('%Y%m%d%H%M%S')+('%05d'%user.id) # 创建一个事务,要么全部操作成功 , 要么全部操作失败 with transaction.atomic(): # 获取数据库最初的状态 save_id = transaction.savepoint() try: order = OrderInfo.objects.create( order_id= order_id, user = user, address=address, total_count = 0, total_amount = 0, freight = 35, pay_method = pay_method, status= OrderInfo.ORDER_STATUS_ENUM['UNPAID'] if pay_method == OrderInfo.PAY_METHODS_ENUM['ALIPAY'] else OrderInfo.ORDER_STATUS_ENUM['UNSEND'] ) # 获取购物车商品数据 redis_conn = get_redis_connection('carts') redis_cart = redis_conn.hgetall('cart_%s' % user.id) redis_selected = redis_conn.smembers('selected_%s' % user.id) # 获取购物车中勾选的状态数据 new_cart_dict = {} for sku_id in redis_selected: new_cart_dict[int(sku_id)] = int(redis_cart[sku_id]) skus = SKU.objects.filter(id__in = new_cart_dict.keys()) #进行单件商品的计算 for sku in skus: sku_count = new_cart_dict[sku.id] # 获取商品的销量和库存 origin_stock= sku.stock origin_sales= sku.sales # 判断商品的库存是否足够 if sku_count > origin_stock: transaction.savepoint_rollback(save_id) return JsonResponse({'code': RETCODE.DBERR, 'errmsg': '商品库存不足'}) # 对库存进行减少 , 销量进行增加 sku.stock -= sku_count sku.sales += sku_count sku.save() # 保存商品订单信息 OrderGoods.objects.create( order=order, sku=sku, count=sku_count, price = sku.price ) # 计算单间商品的购买数量和总金额 order.total_count += sku_count order.total_amount += sku_count * sku.price # 对总金额加入运费 order.total_amount += order.freight order.save() except Exception: # 数据库操作异常 , 事务回滚 transaction.savepoint_rollback(save_id) return JsonResponse({'code': RETCODE.DBERR, 'errmsg': '订单提交失败'}) # 提交事务 transaction.savepoint_commit(save_id) return JsonResponse({'code':RETCODE.OK , 'errmsg':'OK' , 'order_id':order_id})
3、提交订单成功页面
# 我的订单 path('orders/info/' , views.UserOrderInfoView.as_view() , name='myorder') class UserOrderInfoView(View): def get(self , request): page_num = request.GET.get('page_num') user = request.user orders = user.orderinfo_set.all() # 获取 商品订单数据 for order in orders: # 支付方式 , 1 , 2 order.pay_method_name = OrderInfo.PAY_METHOD_CHOICES[order.pay_method - 1][1] # 订单状态 order.status_name = OrderInfo.PAY_METHOD_CHOICES[order.status - 1][1] order.sku_list = [] order_goods = order.skus.all() for order_good in order_goods: sku = order_good.sku sku.count = order_good.count sku.amount = sku.price * sku.count order.sku_list.append(sku) # 制作分页 if not page_num: page_num = 1 page_num = int(page_num) paginatot = Paginator(orders , 5) page_orders = paginatot.page(page_num) total_page = paginatot.num_pages context = { 'page_orders' : page_orders, 'total_page':total_page, 'page_num':page_num } return render(request , 'user_center_order.html' , context=context)
十、项目部署
1、Nginx
Nginx:开源的高性能的HTTP和反向代理服务器
反向代理:服务器做出逆向操作 , 代理服务器接收用户发送的请求,解析转发给内部服务器,返回Response的响应。
WAF功能:阻止 web 攻击
Nginx特点:内存小 , 并发能力强 , 灵活好扩展
2、配置Linux环境
1、要有 Python 环境
2、要有 MySQL 数据库
3、下载 redis 数据库
sudo yum install redis
4、下载 Nginx
sudo yum install epel-release yum install -y nginx
5、下载 UWSGI
sudo yum install epel-release yum install python3-devel pip3.8 install uwsgi==2.0.19.1
6、下载项目需要的所有模块
pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple mysqlclient==2.1.0 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple django==3.2 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pymysql pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pillow==8.3.0 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple ronglian_sms_sdk pip install -i https://pypi.tuna.tsinghua.edu.cn/simple itsdangerous==1.1.0 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple urllib3==1.26.15 pip2 install -i https://pypi.tuna.tsinghua.edu.cn/simple django_redis pip install -i https://pypi.tuna.tsinghua.edu.cn/simple django_haystack pip install -i https://pypi.tuna.tsinghua.edu.cn/simple whoosh pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests 更新 pip版本: pip3 install --upgrade pip 在下载模块前,先下载需要的依赖 yum install python3-devel mysql-devel 项目中需要的模块 mysqlclient==2.1.1 django==3.2 pymysql pillow==8.3.0 ronglian_sms_sdk itsdangerous==1.1.0 urllib3==1.26.15 django_redis django_haystack whoosh requests 出现: Aonther app is ***** exit *** 另一个应用程序***** 执行: rm -f /var/rum/yum.pid
3、项目部署
1、在项目上传到 Linux 之前 , 修改 settings.py 文件 , 允许所有主机访问
ALLOWED_HOSTS = ['*']
2、将搜索索引目录: whoosh_index 删除
3、将整个项目的数据迁移数据库记录文件全部删除
4、通过 Xftp 上传到 Linux 中:opt目录中
5、配置 uwsgi 的配置信息
到 etc 目录下创建 uwsg.d
目录 mkdir uwsgi.d
进入创建的目录中,创建 uwsgi.ini 配置文件: vim uwsgi.ini
[uwsgi] socket= 120.55.47.111:8080 chdir=/opt/ShopSystem module=JiXuShopSystem/wsgi.py processes=2 threads=2 master=True pidfile=uwsgi.pid buffer-size = 65535
6、配置 Nginx
到 etc/nginx/nginx.conf
server { listen 8080; # listen [::]:80; server_name 120.55.47.111:10056; # root /usr/share/nginx/html; # Load configuration files for the default server block. # include /etc/nginx/default.d/*.conf; charset utf-8; location /static { alias /opt/www/django_-shop-system/static; } location / { include uwsgi_params; uwsgi_pass 0.0.0.0:8005; uwsgi_param UWSGI_SCRITP django_-shop-system.wsgi; uwsgi_param UWSGI_CHDIR /opt/www/django_-shop-system; }
7、进入MySQL数据库 , 创建项目需要的数据库。
8、启动
启动 nginx : nginx 启动uwsgi:进入uwsgi.d目录下执行: uwsgi --ini uwsgi.ini 启动redis : systemctl start redis 关闭防火墙:systemctl stop firewalld.service
9、迁移数据库,生成全文搜索索引
python3 manage.py rebuild_index
10、启动项目
python3.8 manage.py runserver 0.0.0.0:8000 一定要有ip和端口号 , 否则外部设备无法访问
遭周文而舒志