0%

本项目是一个典型的电商项目,采用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

新建名为settingspython 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',
},
},
]

新建名为utilspython package二级shop中。

新建名为jinja2_env.pypython fileutils目录中,并在其中写如下代码:

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
mysql -uroot -p111
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"

配置工程日志

新建名为logspython package一级shop

新建名为shop.logfile于目录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')]

配置模板文件

新建名为templatespython package二级shop中,将其标记为Template Folder

项目环境搭建完毕,开启服务器,在浏览器中查看效果

在浏览器中输入地址:127.0.0.1:8000/static/index.html

编写用户模块代码

新建名为appspython 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.pypython 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_codepython fileutils目录中,内容如下:

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

1
2
pip install Pillow

新建名为libspython package目录于二级shop中,将第三方图片验证码工具captcha拷贝至这里。

新建名为verificationsappapps中:

1
2
python ../../manage.py startapp verifications

注册appdev.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')

新建名为constantspy文件于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_taskspython package一级shop

新建名为mainpython filecelery_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',
])

新建名为configpython filecelery_tasks中,内容如下:

1
2
broker_url = 'redis://127.0.0.1:6379/15'

新建名为smspython packagecelery_tasks

新建名为taskspython filesms中,内容如下:

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_backendspython fileutils中,内容如下:

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/appspython ../../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中指定登录页视图:

1
2
LOGIN_URL = '/login/'

改写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.pypython 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.pypython 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):


新建名为contantspython 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/'

新建名为mailpython packagecelery_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',