本网站是基于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
class RegisterView(View):
'''
用户注册
'''
def get(self , request):
return render(request , 'register.html')
使用 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'
实现用户数据提交之后,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('注册失败')
# 校验用户名重复
re_path('^username/(?P[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})
创建一个应用实现验证码的功能 —— 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[\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>
什么时候发送短信验证码
图片验证码校验成功之后发送
发送短信验证码的功能使用: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/(?P1[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 模块文本保存进去
在 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>
# 用户登录
path('login/' , views.LoginView.as_view() , name='login'),
class LoginView(View):
'''
用户登录视图
'''
def get(self , request):
return render(request , 'login.html')
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)
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']
在后端的登录请求的视图中,在用户登录成功之后将用户名写到 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)
退出登录:将用户的数据从 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
# 用户中心
path('info/' , views.UserInfoView.as_view() , name='info')
class UserInfoView(View):
'''
用户中心
'''
def get(self , request):
return render(request , 'user_center_info.html')
使用 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)
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)
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'})
让 Django 发送邮件 , 是无法直接发送,需要借助SMTP服务器进行中转
需要到 Django 项目的配置文件中配置邮箱的需要的信息
# 发送邮件的配置参数
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # 指定邮件后端
EMAIL_HOST = 'smtp.163.com' # 发邮件主机
EMAIL_PORT = 25 # 发邮件的端口
EMAIL_HOST_USER = '[email protected]' # 授权邮箱
EMAIL_HOST_PASSWORD = 'BHPRRXBTMTCTGHVU' # 邮箱授权时获取的密码,非登录邮箱的密码
EMAIL_FROM = 'AC-<[email protected]>' # 发件人抬头
# 设置邮箱的激活连接
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-<[email protected]>'
recipient_list = ['[email protected]',]
html_message = '阿宸真的超级帅
'
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'您的邮箱为:
{email} , 请点击链接进行验证激活邮箱' \
f'">{veerify_url}'
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')
class AddressView(View):
'''
用户收货地址
'''
def get(self , request):
return render(request , 'user_center_site.html')
创建一个应用来操作实现地区数据 —— 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>
让其他模型类可以共用时间的字段,在项目中的 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')
# 修改密码
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
# 新增收货地址
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})
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>20b>个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>
# 修改、删除收货地址
re_path('^addresses/(?P\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': '删除地址成功'})
# 设置用户默认收货地址
re_path('^addresses/(?P\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:{
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>
因为商品列表中页需要商品分类的功能 , 将首页中的商品分类功能进行抽取出来单独定义一个模块中,需要导入调用。
在 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>
价格a>
人气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>
# 热销商品排行
re_path('^hot/(?P\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>
# 商品详情页
# 这个是哪一个商品:sku.id
re_path('^detail/(?P\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>
# 统计分类商品访问量
re_path('^detail/visit/(?P\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"})
全文搜索引擎
需要下载这两个框架
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>
创建应用实现购物车的功能操作 —— 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'})
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>
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})
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"})
# 全选购物车
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"})
用户浏览记录临时数据 , 数据读写频繁 , 存放在 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"})
创建应用实现订单的功能 —— 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>
# 提交订单
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})
# 我的订单
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)
Nginx:开源的高性能的HTTP和反向代理服务器
反向代理:服务器做出逆向操作 , 代理服务器接收用户发送的请求,解析转发给内部服务器,返回Response的响应。
WAF功能:阻止 web 攻击
Nginx特点:内存小 , 并发能力强 , 灵活好扩展
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
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和端口号 , 否则外部设备无法访问
遭周文而舒志