本项目是一个典型的电商项目,采用Python
语言编写,Django
框架搭建。
在github
中创建远程仓库 在github
上创建仓库meiduo
,填写描述,选择.gitignore
文件的语言为python
,软件许可协议为MIT
。
修改.gitignore
的内容为:.idea/
,*.pyc
,*.log
。
新建分支dev
。
在本地克隆项目 1 2 3 4 5 git clone https://github.com/junsircoding/meiduo.git # 克隆项目 cd meiduo # 进入项目目录 git branch # 查看当前分支 git branch dev origin/dev # 克隆远程仓库中的dev分支 git checkout dev # 切换到dev分支
在虚拟环境中创建项目 1 2 3 4 workon django_py3_1.11 # 进入虚拟环境 django-admin startproject shop # 创建项目,项目名称为shop cd shop # 进入项目目录 pwd # 查看当前地址并拷贝,在pycharm中打开
在Pycharm中搭建项目 重设settings
文件路径
开发环境和上线环境用不同的配置文件比较容易部署和维护,故而最好重新设置settings
的路径。
在django
自动创建的项目中,根目录下有一个同名目录,在此做一个约定:根目录的shop
为一级shop
,根目录下的同名目录为二级shop
。
新建名为settings
的python package
于二级shop中,将原二级shop中的settings.py
更名为dev.py
,并将其移动到新建的settings
目录中。
修改一级shop下的manage.py
中的环境变量:
1 2 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shop.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shop.settings.dev")
配置jinja2
模板 在dev.py
(位于二级shop)中配置jinja2
的模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 TEMPLATES = [ { 'BACKEND': 'django.template.backends.jinja2.Jinja2', 'DIRS': [os.path.join(BASE_DIR, 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], 'environment': 'meiduo_mall.utils.jinja2_env.jinja2_environment', }, }, ]
新建名为utils
的python package
于二级shop
中。
新建名为jinja2_env.py
的python file
于utils
目录中,并在其中写如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from django.contrib.staticfiles.storage import staticfiles_storage from django.urls import reverse from jinja2 import Environment def jinja2_environment(**options): env = Environment(**options) env.globals.update({ 'static': staticfiles_storage.url, 'url': reverse, }) return env """ 确保可以使用Django模板引擎中的{% url('') %} {% static('') %}这类的语句 """
配置mysql
数据库 新建mysql数据库
1 2 3 4 create database shop charset=utf8; create user shoproot identified by '111'; grant all on shop.* to 'shop'@'%'; flush privileges;
配置mysql
数据库于dev.py
中
1 2 3 4 5 6 7 8 9 10 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'HOST': '127.0.0.1', 'PORT': 3306, 'USER': 'shoproot', 'PASSWORD': '111', 'NAME': 'shop' }, }
修改__init__.py
(二级shop目录),配置pymysql
连接 1 2 3 from pymysql import install_as_MySQLdb install_as_MySQLdb()
配置Redis
中的缓存
及Session
在dev.py
(二级shop/settings)中添加如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/0", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } }, "session": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/1", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } }, } SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_CACHE_ALIAS = "session"
配置工程日志 新建名为logs
的python package
于一级shop
中
新建名为shop.log
的file
于目录logs
中
在dev.py
(二级shop/settings)中添加日志的配置信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s' }, 'simple': { 'format': '%(levelname)s %(module)s %(lineno)d %(message)s' }, }, 'filters': { 'require_debug_true': { '()': 'django.utils.log.RequireDebugTrue', }, }, 'handlers': { 'console': { 'level': 'INFO', 'filters': ['require_debug_true'], 'class': 'logging.StreamHandler', 'formatter': 'simple' }, 'file': { 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', 'filename': os.path.join(os.path.dirname(BASE_DIR), 'logs/shop.log'), 'maxBytes': 300 * 1024 * 1024, 'backupCount': 10, 'formatter': 'verbose' }, }, 'loggers': { 'django': { 'handlers': ['console', 'file'], 'propagate': True, 'level': 'INFO', }, } }
配置静态页面 将静态页面文件拷贝到二级shop目录下
在dev.py
中配置静态文件路径
1 2 3 4 5 STATIC_URL = '/static/' STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
配置模板文件 新建名为templates
的python package
于二级shop
中,将其标记为Template Folder
项目环境搭建完毕,开启服务器,在浏览器中查看效果 在浏览器中输入地址:127.0.0.1:8000/static/index.html
编写用户模块代码 新建名为apps
的python package
于二级shop
中
进入apps
目录,用Django
命令创建子应用:
1 2 3 cd apps python ../../manage.py startapp users
在dev.py
中注册app
,在二级shop/apps/users/apps.py
中,右击app名称sConfig
,选择Copy Reference
,拷贝引用,在dev.py
中粘贴
1 2 ‘shop.apps.users.apps.UsersConfig’,
在dev.py
中追加导包路径
1 2 3 import sys sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
改写注册内容:
1 2 ‘users.apps.UsersConfig’,
新建名为urls.py
的python file
于二级shop/apps/users
中,在其中填写如下内容:
1 2 3 4 5 6 7 from django.conf.urls import url from . import views urlpatterns = [ url(r'^register/$', views.RegisterView.as_view()), ]
将此子路由
添加至总路由
(二级shop/urls.py):
1 2 3 4 5 6 7 8 from django.conf.urls import url, include from django.contrib import admin urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^', include('users.urls')), ]
编写视图函数RegisterView
在二级shop/apps/users/views.py
中:
1 2 3 4 5 6 7 8 9 from django.shortcuts import render from django.views import View class RegisterView(View): def get(self, request): return render(request, 'register.html')
编写用户模型类
Django
自带了用户模型类,如要添加别的字段,只需继承Django自带的模型类,再添加自己的特有字段即可
在二级shop/apps/users/models.py
中添加如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): """自定义用户模型类""" mobile = models.CharField(max_length=11, unique=True, verbose_name='手机号') class Meta: db_table = 'tb_users' verbose_name = '用户' verbose_name_plural = verbose_name def __str__(self): return self.username
在dev.py
中指定用户模型类:
1 2 AUTH_USER_MODEL = 'users.User'
迁移用户模型类 创建迁移文件
1 2 python manage.py makemigrations
执行迁移文件
1 2 python manage.py migrate
编码 注册功能
填写完注册页面表单后,后台要处理POST
请求
在二级shop/apps/users/views.py/RegisterView
中添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 def post(self, request): user_name = request.POST.get('user_name') pwd = request.POST.get('pwd') cpwd = request.POST.get('cpwd') phone = request.POST.get('phone') allow = request.POST.get('allow') if not all([user_name, pwd, cpwd, phone, allow, sms_code_request]): return http.HttpResponseBadRequest('参数不完整') if not re.match(r'^[a-zA-Z0-9_-]{5,20}$', user_name): return http.HttpResponseBadRequest('请输入5-20个字符') if User.object.filter(username=user_name).count() > 0: return http.HttpResponseBadRequest('用户名已存在') if not re.match(r'[0-9A-Za-z]{8,20}$', pwd): return http.HttpResponseBadRequest('请输入8-20的密码') if pwd != cpwd: return http.HttpResponseBadRequest('两次密码输入不一致') if not re.match(r'^1[345789]\d{9}', phone): return http.HttpResponseBadRequest('手机号格式不正确') if User.objects.filter(mobile=phone).count() > 0: return http.HttpResponseBadRequest('手机号已存在') user = User.objects.create_user(username=user_name, password=pwd, mobile=phone) login(request, user) return redirect('/')
用Ajax异步校验的方式验证用户名是否存在 在子路由中添加ajax
的路由:
1 2 url(r'^usernames/(?P<username>[a-zA-Z0-9_-]{5,20})/count/$', views.UsernameCountView.as_view()),
在二级shop/apps/users/views.py
中添加视图类:
1 2 3 4 5 6 7 8 9 10 11 12 13 class UsernameCountView(View): def get(self, request, username): count = User.objects.filter(username=username).count() return http.JsonResponse({ 'code':RETCODE.OK, 'errmsg':'OK', 'count':count })
用Ajax异步校验的方式验证手机号是否存在 在子路由中添加ajax
的路由:
1 2 url('^mobiles/(?P<mobile>1[3-9]\d{9})/count/$', views.MobileCountView.as_view()),
在二级shop/apps/users/views.py
中添加视图类:
1 2 3 4 5 6 7 8 9 10 11 12 13 class MobileCountView(View): def get(self, request, mobile): count = User.objects.filter(mobile=mobile).count() return http.JsonResponse({ 'code':RETCODE.OK, 'errmsg':'OK', 'count':count })
新建名为response_code
的python file
于utils
目录中,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class RETCODE: OK = "0" IMAGECODEERR = "4001" THROTTLINGERR = "4002" NECESSARYPARAMERR = "4003" USERERR = "4004" PWDERR = "4005" CPWDERR = "4006" MOBILEERR = "4007" SMSCODERR = "4008" ALLOWERR = "4009" SESSIONERR = "4101" DBERR = "5000" EMAILERR = "5001" TELERR = "5002" NODATAERR = "5003" NEWPWDERR = "5004" OPENIDERR = "5005" PARAMERR = "5006" STOCKERR = "5007"
图片验证码 安装pillow
新建名为libs
的python package
目录于二级shop
中,将第三方图片验证码工具captcha
拷贝至这里。
新建名为verifications
的app
于apps
中:
1 2 python ../../manage.py startapp verifications
注册app
于dev.py
中:
1 2 'verifications.apps.VerificationsConfig',
新建路由表urls
,在子路由中添加路由:
1 2 3 4 urlpatterns = [ url(r'^image_codes/(?P<uuid>[\w-]+)/$', views.ImagecodeView.as_view()), ]
在总路由中包含子路由:
1 2 3 4 urlpatterns = [ url(r'^', include('verifications.urls')), ]
编写视图函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class ImagecodeView(View): def get(self, request, uuid): text, code, image = captcha.generate_captcha() redis_cli = get_redis_connection('verify_code') redis_cli.setex(uuid, constants.IMAGE_CODE_EXPIRES, code) return http.HttpResponse(image, content_type='image/png')
新建名为constants
的py
文件于verifications
目录下,编写其内容如下:
1 2 3 4 5 6 7 IMAGE_CODE_EXPIRES = 60 * 5 SMS_CODE_EXPIRES = 60 * 5 SMS_CODE_FLAG_EXPIRES = 60
验证码字符在redis
缓存中存储,在dev.py
中新建缓存字段verify_code
:
1 2 3 4 5 6 7 8 "verify_code":{ "BACKEND":"django_redis.cache.RedisCache", "LOCATION":"redis://127.0.0.1:6379/2", "OPTIONS":{ "CLIENT_CLASS":"django_redis.client.DefaultClient", } },
注意:chrome有个大坑,它会缓存之前请求过的地址。验证码的请求地址是和host.js关联的。当更改了host.js时,重新访问发现地址并没有更改,这是chrome缓存的缘故,记得定期清理缓存。
短信验证码 拷贝工具代码yuntongxun(短信)
至子shop/libs
目录中
在verifications/url
中添加路由:
1 2 3 4 5 6 7 8 from django.conf.urls import url from . import views urlpatterns = [ url(r'^image_codes/(?P<uuid>[\w-]+)/$', views.ImagecodeViews.as_view()), url('^sms_codes/(?P<mobile>1[3-9]\d{9})/$', views.SmscodeView.as_view()), ]
在verifications/views.py
中添加名为SmscodeView
的视图函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class SmscodeView(View): def get(self, request): image_code_request = request.GET.get('image_code') uuid = request.GET.get('image_code_id') redis_cli = get_redis_connection('verify_code') image_code_redis = redis_cli.get(uuid) if not image_code_redis: return http.JsonResponse({ 'code':RETCODE.PARAMERR, 'errmsg':'图形验证码已过期' }) redis_cli.delete(uuid) if image_code_redis.decode() != image_code_request.upper(): return http.JsonResponse({ 'code':RETCODE.PARAMERR, 'errmsg':'图形验证码错误' }) sms_code = '%6d' % random.randint(0, 999999) redis_cli.setex('sms_'+mobile, 300, sms_code) print(sms_code) return http.JsonResponse({ 'code':RETCODE.OK, 'errmsg':'OK' })
在注册视图中验证短信验证码,改写二级shop/apps/users/views.py
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 def post(self, request): user_name = request.POST.get('user_name') pwd = request.POST.get('pwd') cpwd = request.POST.get('cpwd') phone = request.POST.get('phone') allow = request.POST.get('allow') sms_code_request = request.POST.get('msg_code') if not all([user_name, pwd, cpwd, phone, allow, sms_code_request]): return http.HttpResponseBadRequest('参数不完整') if not re.match(r'^[a-zA-Z0-9_-]{5,20}$', user_name): return http.HttpResponseBadRequest('请输入5-20个字符') if User.objects.filter(username=user_name).count() > 0: return http.HttpResponseBadRequest('用户名已存在') if not re.match(r'[0-9A-Za-z]{8,20}$', pwd): return http.HttpResponseBadRequest('请输入8-20的密码') if pwd != cpwd: return http.HttpResponseBadRequest('两次密码输入不一致') if not re.match(r'^1[345789]\d{9}', phone): return http.HttpResponseBadRequest('手机号格式不正确') if User.objects.filter(mobile=phone).count() > 0: return http.HttpResponseBadRequest('手机号已存在') redis_cli = get_redis_connection('verify_code') sms_code_redis = redis_cli.get('sms_' + phone) if not sms_code_redis: return http.HttpResponseBadRequest('短信验证已过期') redis_cli.delete('sms_' + phone) if sms_code_redis != sms_code_request: return http.HttpResponseBadRequest('短信验证码错误') user = User.objects.create_user(username=user_name, password=pwd, mobile=phone) login(request, user) return redirect('/')
避免频繁发送短信,添加如下代码于verificatons/views.py
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 if image_code_redis.decode() != image_code_request.upper(): return http.JsonRequest({ 'code':RETCODE.PARAMERR, 'errmsg':'图形验证码错误' }) if redis_cli.get('sms_flag_' + mobile): return http.JsonResponse({ 'code':RETCODE.PARAMERR, 'errmsg':'已经向次手机号发过短信,请查看手机' }) sms_code = '%6d' % random.randint(0, 999999) redis_cli.setex('sms_'+mobile, constants.SMS_CODE_EXPIRES, sms_code) redis_cli.setex('sms_flag_' + mobile, constants.SMS_CODE_FLAG_EXPIRES, 1) print(sms_code)
使用pipeline
优化与redis
交互,只与redis
服务器交互一次,执行多条命令
1 2 3 4 5 6 7 8 9 10 11 12 redis_pl = redis_cli.pipeline() redis_pl.setex('sms_'+mobile, constants.SMS_CODE_EXPIRES, sms_code) redis_pl.setex('sms_flag_' + mobile, constants.SMS_CODE_FLAG_EXPIRES, 1) redis_pl.execute()
使用celery
框架实现异步 新建名为celery_tasks
的python package
于一级shop
中
新建名为main
的python file
于celery_tasks
中,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 from celery import Celery import os os.environ["DJANGO_SETTINGS_MODULE"] = "shop.settings.dev" app = Celery('shop') app.config_from_object('celery_tasks.config') app.autodiscover_tasks([ 'celery_tasks.sms', ])
新建名为config
的python file
于celery_tasks
中,内容如下:
1 2 broker_url = 'redis://127.0.0.1:6379/15'
新建名为sms
的python package
于celery_tasks
中
新建名为tasks
的python file
于sms
中,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 from shop.libs.yuntongxun.sms import CCP from celery_tasks.main import app @app.task(bind=True, name='send_sms', retry_backoff = 3) def send_sms(self, to, datas, tempid): try: print(datas[0]) except Exception as e: self.retry(exc = e, max_retries = 3)
在shell
中运行如下命令,开启celery
服务:
1 2 celery -A celery_tasks.main worker -l info
调用任务,于二级shop/apps/users/views.py
中:
1 2 3 4 5 6 7 from celery_tasks.sms.tasks import send_sms send_sms.delay(mobile, [sms_code, contansts.SMS_CODE_EXPIRES / 60], 1)
登录功能 在二级shop/apps/users/urls.py
中添加登录的路由:
1 2 url('^login/$', views.LoginView.as_view()),
在二级shop/apps/users/views.py
中添加视图类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class LoginView(View): def get(self, request): return render(request, 'login.html') def post(self, request): username = request.POST.get('username') pwd = request.POST.get('pwd') if not all([user_name, pwd]): return http.HttpResponseBadRequest('参数不完整') if not re.match(r'^[a-zA-Z0-9_-]{5,20}$', user_name): return http.HttpResponseBadRequest('请输入5-20个字符') if not re.match(r'[0-9A-Za-z]{8,20}$', pwd): return http.HttpResponseBadRequest('请输入8-20的密码') user = authenticate(username = username, pwd = pwd) if user is None: return render(request, 'login.html', { 'loginerror':'用户名或密码错误' }) else: login(request, user) return redirect('/')
多账号登录 新建名为shop_backends
的python file
于utils
中,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from django.contrib.auth.backends import ModelBackend class ShopModelBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): try: if re.match('^1[3-9]\d{9}$', username): user = User.objects.get(mobile=username) else: user = User.objects.get(username=username) except: return None else: if user.check_password(password): return user else: return None
在dev.py
中添加自定义认证类型:
1 2 AUTHENTICATION_BACKENDS = ['shop.utils.auth_backends.ShopModelBackend']
首页用户名显示 新建应用,cd shop/shop/apps
,python ../../manage.py startapp contents
注册应用,新建urls.py
:
1 2 3 4 5 6 7 from django.conf.urls import url from . import views urlpatterns = [ url('^$', views.IndexView.as_view()), ]
在dev.py
中注册应用:
1 2 'contents.apps.ContentsConfig',
在总路由中添加子路由:
1 2 url('^', include('content.urls')),
添加视图类:
1 2 3 4 5 6 from django.shortcuts import render from django.views import View class IndexView(View): def get(self, request): return render(request, 'index.html')
更改users/views.py.LoginView
:
1 2 3 4 5 6 7 8 else: login(request, user) response = redirect('/') response.set_cookie('username', user.username, max_age=60*60*24*14) return response
更改users/views.py.RegisterView
:
1 2 3 4 5 response = redirect('/') response.set_cookie('username', user.username, max_age=60*60*24*14) return response
退出 在users/views.py
中添加新的视图类:
1 2 3 4 5 6 7 8 9 class LogoutView(View): def get(self, request): logout(request) response = redirect('/') response.delete_cookie('usename') return response
在users.urls.py
中添加路由:
1 2 url('^logout/$', views.LogoutView.as_view()),
用户个人信息 添加路由在users/urls.py
中:
1 2 url('^info/$', views.InfoView.as_view()),
添加视图类在users/views.py
中:
1 2 3 4 5 6 7 8 9 10 class InfoView(View): def get(self, request): if request.user.is_authenticated: return render(request, 'user_center_info.html') else: return redirect('/login/')
拷贝页面至templates
中。
判断是否登录 在dev.py
中指定登录页视图:
改写views.py
中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 from django.contrib.auth.mixins import LoginRequiredMixin class InfoView(LoginRequiredMixin, View): def get(self, request): return render(request, 'user_center_info.html')
完善登录视图代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class LoginView(View): def get(self, request): return render(request, 'login.html') def post(self, request): username = request.POST.get('username') pwd = request.POST.get('pwd') next_url = request.GET.get('next', '/') if not all([user_name, pwd]): return http.HttpResponseBadRequest('参数不完整') if not re.match(r'^[a-zA-Z0-9_-]{5,20}$', user_name): return http.HttpResponseBadRequest('请输入5-20个字符') if not re.match(r'[0-9A-Za-z]{8,20}$', pwd): return http.HttpResponseBadRequest('请输入8-20的密码') user = authenticate(username = username, pwd = pwd) if user is None: return render(request, 'login.html', { 'loginerror':'用户名或密码错误' }) else: login(request, user) response = redirect(next_url) response.set_cookie('username', user.username, max_age=60*60*24*14) return response
QQ授权登录 在虚拟环境中安装QQLoginTool
1 2 pip install QQLoginTool
新建名为oauth
的应用,python ../../manage.py startapp oauth
新建urls.py
,内容如下:
1 2 3 4 5 6 7 from django.conf.urls import url from . import views urlpatterns = [ url('^qq/login/$', views.QQurlView.as_view()), url('^oauth_callback$', views.QQopenidView.as_view()), ]
在总路由中添加子路由:
1 2 url('^', include('oauth.urls')),
在dev.py
中注册app
1 2 'oauth.apps.OauthConfig',
在views.py
中创建类视图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 from django.shortcuts import render from django.views import View from django import http from QQLoginTool.QQtool import OAuthQQ from django.conf import settings class QQurlView(View): def get(self, request): next_url = request.GET.get('next', '/') oauthqq_tool = OAuthQQ( settings.QQ_CLIENT_ID, settings.QQ_CLIENT_SECRET, settings.QQ_CLIENT_URI, next_url ) login_url = oauthqq_tool.get_qq_url() return http.JsonResponse({ 'code':RETCODE.OK, 'errmsg':'OK', 'login_url':login_url }) class QQopenidView(View): def get(self, request): code = request.GET.get('code') next_url = request.GET.get('state', '/') oauthqq_tool = OAuthQQ( settings.QQ_CLIENT_ID, settings.QQ_CLIENT_SECRET, settings.QQ_CLIENT_URI, next_url ) try: token = oauthqq_tool.get_access_token(code) openid = oauthqq_tool.get_openid(token) except: openid = '0' return http.HttpResponse(openid)
在dev.py
中添加QQ授权信息:
1 2 3 4 QQ_CLIENT_ID = '101518219' QQ_CLIENT_SECRET = '418d84ebdc7241efb79536886ae95224' QQ_REDIRECT_URI = 'http://www.meiduo.site:8000/oauth_callback'
在/etc/hosts
中添加127.0.0.1 www.meiduo.site
在dev.py
中添加:ALLOWED_HOSTS = ['www.meiduo.site',]
将host.js
中的var host = 'http://www.meiduo.site:8000'
的注释打开,其余均注释
QQ账号信息与本网站绑定 新建视图类于oauth/model.py
中:
1 2 3 4 5 6 7 8 9 10 from users.models import User from shop.utils.models import BaseModel class OAuthQQUser(models.Model): user = models.ForeignKey(User) openid = models.CharField(max_length = 50) class Meta: db_table = 'tb_oauth_qq'
新建名为models.py
的python file
文件于utils
目录中,内容为:
1 2 3 4 5 6 7 8 9 from django.db import models class BaseModel(models.Model): create_time = models.DateField(auto_now_add = True) update_time = models.DateField(auto_now = True) class Meta: abstract = True
执行迁移:
1 2 3 python manage.py makemigrations python manage.py migrate
改写oauth/views.py
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 from django.shortcuts import render from django.views import View from django import http from QQLoginTool.QQtool import OAuthQQ from django.conf import settings from .models import OAuthQQUser class QQurlView(View): def get(self, request): next_url = request.GET.get('next', '/') oauthqq_tool = OAuthQQ( settings.QQ_CLIENT_ID, settings.QQ_CLIENT_SECRET, settings.QQ_CLIENT_URI, next_url ) login_url = oauthqq_tool.get_qq_url() return http.JsonResponse({ 'code':RETCODE.OK, 'errmsg':'OK', 'login_url':login_url }) class QQopenidView(View): def get(self, request): code = request.GET.get('code') next_url = request.GET.get('state', '/') oauthqq_tool = OAuthQQ( settings.QQ_CLIENT_ID, settings.QQ_CLIENT_SECRET, settings.QQ_CLIENT_URI, next_url ) try: token = oauthqq_tool.get_access_token(code) openid = oauthqq_tool.get_openid(token) try: qquser = OAuthQQUser.objects.get(openid=openid) except: return render(request, 'oauth_callback.html') except: openid = '0' return http.HttpResponse(openid)
在views.py
中添加post方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class QQopenidView(View): def get(self, request): code = request.GET.get('code') next_url = request.GET.get('state', '/') oauthqq_tool = OAuthQQ( settings.QQ_CLIENT_ID, settings.QQ_CLIENT_SECRET, settings.QQ_CLIENT_URI, next_url ) try: token = oauthqq_tool.get_access_token(code) openid = oauthqq_tool.get_openid(token) try: qquser = OAuthQQUser.objects.get(openid=openid) except: context = {'token':openid} return render(request, 'oauth_callback.html', context) except: openid = '0' return http.HttpResponse(openid) def post(self, request):
安装加密的包:itsdangerous
1 2 pip install itsdangerous
新建名为shop_signature.py
的python file
文件于utils.py
中,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 from itsdangerous import TimedJSONWebSignatureSerializer as Serializer from django.conf import settings def dumps(json, expires): ''' :param json:字典 :param expires:加密数据的过期时间 :return:字符串 ''' serializer = Serializer(settings.SECRET_KEY, expires) serializer.dumps(json) return s1.decode() def loadds(s1, expires): ''' :param s1:字符串 :param expires:加密数据的过期时间 :return:字典 ''' serializer = Serializer(settings.SECRET_KEY, expires) try: json = serializer.loads(s1) except: return None return json
更改views.py
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class QQopenidView(View): def get(self, request): code = request.GET.get('code') next_url = request.GET.get('state', '/') oauthqq_tool = OAuthQQ( settings.QQ_CLIENT_ID, settings.QQ_CLIENT_SECRET, settings.QQ_CLIENT_URI, next_url ) try: token = oauthqq_tool.get_access_token(code) openid = oauthqq_tool.get_openid(token) try: qquser = OAuthQQUser.objects.get(openid=openid) except: token = shop_signature.dumps({'openid':openid}, contants.OPENID_EXPIRES) context = {'token':openid} return render(request, 'oauth_callback.html', context) except: openid = '0' return http.HttpResponse(openid) def post(self, request):
新建名为contants
的python file
文件于oauth
目录中,内容如下:
1 2 3 OPENID_EXPIRES = 60 * 10
写post方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 def post(self, request): mobile = request.POST.get('mobile') pwd = request.POST.get('pwd') sms_code_request = request.POST.get('sms_code') access_token = request.POST.get('access_token') next_url = request.GET.get('state') json = shop_signature.loadds(access_token, contants.OPENID_EXPIRES) if json is None: return http.HttpResponseBadRequest('授权信息无效,请重新授权') openid = json.get('openid') try: user = User.objects.get(mobile = mobile) except: user = User.objects.create_user(username=mobile, password=pwd, mobile=mobile) else: if not user.check_password(pwd): return http.HttpResponseBadRequest('密码错误') OAuthQQUSer.objects.create(user=user, openid=openid) login(request, user) response = redirect(next_url) response.set_cookie('username', user.username, max_age = 60 * 60 * 24 * 14) return response
非初次授权的情况。补充get方法:
1 2 3 4 5 6 login(request, qquser.user) response = redirect(next_url) response.set_cookie('username', qquser.user.username, max_age = 60 * 60 * 24 * 14) return response
显示用户的个人信息 改写users/views.py/InfoView
:
1 2 3 4 5 6 7 8 9 10 11 12 def InfoView(LoginRequiredMixin, View): def get(self, request): user = request.user context = { 'username':user.username, 'mobile':user.mobile, 'email':user.email, 'email_active':user.email_active }
在models.py/User
中添加邮箱激活属性:
1 2 3 4 5 class User(AbstractUser): mobile = model.CharField(max_length=11) email_active = models.BooleanField(default=False)
迁移:
1 2 3 python manage.py makemigratons python manage.py migrate
邮箱 在users/urls.py
中添加路由:
1 2 url('^emails$', views.EmailView.as_view()),
在users.views.py
中添加视类图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class EmailView(LoginRequiredMixin, View): def put(self, request): dict1 = json.loads(request.body.decode()) email = dict1.get('email') if not all([email]): return http.JsonResponse({ 'code':RETCODE.PARAMERR, 'errmsg':'没有邮箱参数' }) if not re.match('^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email): return http.JsonResponse({ 'code':RETCODE.PARAMERR, 'errmsg':'邮箱格式错误' }) user = request.user user.email = email user.save() return http.JsonResponse({ 'code':RETCODE.OK, 'errmsg':'OK' })
发邮件 在dev.py
中添加邮件服务器配置:
1 2 3 4 5 6 7 8 EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = 'smtp.163.com' EMAIL_PORT = 25 EMAIL_HOST_USER = 'hmmeiduo@163.com' EMAIL_HOST_PASSWORD = 'hmmeiduo123' EMAIL_FROM = '美多商城<hmmeiduo@163.com>' EMAIL_VERIFY_URL = 'http://www.meiduo.site:8000/emails/verification/'
新建名为mail
的python package
于celery_tasks
目录中,在其中新加文件tasks.py
在其中定义方法:
1 2 3 4 5 6 7 8 9 10 11 from django.core.mail import send_mail from django.conf import settings from celery_tasks.main import app @app.task(name='send_user_email', bind=True) def send_user_email(to_mail, verify_url): html_message = '您的邮箱为:%s,激活链接为%s' % (to_mail, verify_url) try: send_mail('美多商城-邮箱激活','', settings.EMAIL_FROM, [to_mail], html_message=html_message) except Exception as e: self.retry(exc=e, max_retries=2)
在celery_tasks/sms/main.py
中添加任务:
1 2 3 4 5 app.autodiscover_tasks([ 'celery_tasks.sms', 'celery_tasks.mail', ])
启动celery:celery -A celery tasks.main worker -l info
在views.py
中调用任务
1 2 3 4 5 6 7 from celery_tasks.mail.tasks import send_user_email ... token = shop_signature.dumps({'user_id':user_id}, contants.EMAIL_EXPIRES) verify_url = settings.EMAIL_VERIFY_URL + '?token=%s' % token send_user_email.delay(email, verify_url)
在users
,目录总新建contants.py
,内容为:
1 2 3 EMAIL_EXPIRES = 60 * 60 * 2
激活邮箱 新建视图类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class EmailVerifyView(View): def get(self, request): token = request.GET.get('token') dict1 = meiduo_signature.loadds(token, contants.EMAIL_EXPIRES) if dict1 is None: return http.HttpResponseBadRequest('激活信息无效,请重新发邮件') user_id = dict1.get('user_id') try: user = User.objects.get(pk=user_id) except: return http.HttpResponseBadRequest('激活信息无效') else: user.email_active = True user.save() return redirect('/info/')
添加路由:
1 2 url('^emails/verifyication$', views.EmailVerifyView.as_view()),
收货地址 在users/urls.py
中添加路由:
1 2 url('^addresses$', views.AddressesView.as_view()),
在users/views.py
中定义视图类:
1 2 3 4 class AddressesView(LoginRequiredMixin, View): def get(self, request): return render(request, 'user_center_site.html')
新建应用
1 2 3 cd shop/shop/apps/ python ../../manage.py startapp areas
新建路由urls.py
1 2 3 4 5 6 7 from django.conf.urls import url from . import views urlpatterns = [ ]
在总路由中添加子路由
1 2 url('^', include('areas.urls')),
注册app
1 2 'areas.apps.AreasConfig',