# s25 **Repository Path**: energylu/s25 ## Basic Information - **Project Name**: s25 - **Description**: 老男孩django项目实战:轻量级bug管理平台 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-04-18 - **Last Updated**: 2022-09-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README autoauto- [项目概述](#项目概述)auto - [1. 同步运行环境](#1-同步运行环境)auto - [1.1 建立虚拟环境,同步python版本](#11-建立虚拟环境同步python版本)auto - [1.2 同步项目依赖包](#12-同步项目依赖包)auto - [2. 独立配置文件](#2-独立配置文件)auto - [3. 腾讯云平台](#3-腾讯云平台)auto - [4. redis](#4-redis)auto - [5. 项目开发分期](#5-项目开发分期)auto- [项目开发](#项目开发)auto - [1. 前戏](#1-前戏)auto - [1.1 项目虚拟环境](#11-项目虚拟环境)auto - [1.2 项目框架](#12-项目框架)auto - [1.3 git实战应用](#13-git实战应用)auto - [1.3.1 创建远程代码仓库](#131-创建远程代码仓库)auto - [1.3.2 本都代码推送到远程仓库](#132-本都代码推送到远程仓库)auto - [1.4 通过python结合腾讯sms发送短信](#14-通过python结合腾讯sms发送短信)auto - [1.5 Django的ModleForm组件](#15-django的modleform组件)auto - [1.5.1 ModleForm应用场景](#151-modleform应用场景)auto - [1.5.2 实现过程](#152-实现过程)auto - [1.5.3 页面美化](#153-页面美化)auto - [1.5.4 注册实现](#154-注册实现)auto - [1.6 redis](#16-redis)auto - [1.6.1 安装redis软件](#161-安装redis软件)auto - [1.6.2 安装redis模块](#162-安装redis模块)auto - [1.6.3 Django-redis模块](#163-django-redis模块)auto - [2. 实现注册](#2-实现注册)auto - [2.1 展示注册页面](#21-展示注册页面)auto - [2.1.1 创建web应用](#211-创建web应用)auto - [2.1.2 创建模板文件目录](#212-创建模板文件目录)auto - [2.1.3 母版准备](#213-母版准备)auto - [2.1.4 url准备](#214-url准备)auto - [2.1.5 form表单](#215-form表单)auto - [2.2 点击获取验证码](#22-点击获取验证码)auto - [2.2.1 绑定点击事件](#221-绑定点击事件)auto - [2.2.2 获取手机号](#222-获取手机号)auto - [2.2.3 发送ajax请求](#223-发送ajax请求)auto - [2.2.4 手机号格式校验](#224-手机号格式校验)auto - [2.2.5 验证通过](#225-验证通过)auto - [2.2.6 发送成功或失败](#226-发送成功或失败)auto - [2.3 点击注册](#23-点击注册)auto - [2.3.1 注册按钮绑定事件](#231-注册按钮绑定事件)auto - [2.3.2 数据校验](#232-数据校验)auto - [2.3.3 写入数据库](#233-写入数据库)auto - [3. 短信登录](#3-短信登录)auto - [3.1 展示页面](#31-展示页面)auto - [3.2 点击发送短信](#32-点击发送短信)auto - [3.3 点击登录](#33-点击登录)auto - [3.4 补充](#34-补充)auto - [4. 用户名密码登陆](#4-用户名密码登陆)auto - [4.1 python生成验证码图片](#41-python生成验证码图片)auto - [4.2 Session & Cookie](#42-session--cookie)auto - [4.3 页面显示](#43-页面显示)auto - [4.4 登陆](#44-登陆)auto - [4.5 登陆成功中间件](#45-登陆成功中间件)auto- [二期](#二期)auto - [1. 管理中心概述](#1-管理中心概述)auto - [1.1 django离线脚本](#11-django离线脚本)auto - [1.2 探讨业务](#12-探讨业务)auto - [1.2.1 价格策略,建表](#121-价格策略建表)auto - [1.2.2 用户](#122-用户)auto - [1.2.3 交易表](#123-交易表)auto - [1.2.4 创建存储](#124-创建存储)auto - [1.2.5 项目](#125-项目)auto - [1.2.6 项目参与者](#126-项目参与者)auto - [1.3 设计表结构](#13-设计表结构)auto - [1.4 添加项目](#14-添加项目)auto - [1.4.1 项目列表母版+样式](#141-项目列表母版样式)auto - [1.4.2 添加项目](#142-添加项目)auto - [1.4.3 查看项目列表](#143-查看项目列表)auto - [1.4.4 星标](#144-星标)auto - [1.5 功能实现](#15-功能实现)auto - [1.6 wiki,项目文档库](#16-wiki项目文档库)auto - [1.6.1 表结构设计](#161-表结构设计)auto - [1.6.2 快速开发](#162-快速开发)auto - [1.6.3 应用markdown组件](#163-应用markdown组件)auto - [1.6.4 腾讯COS做上传](#164-腾讯cos做上传)auto - [2. 文件管理](#2-文件管理)auto - [2.1 知识点](#21-知识点)auto - [2.2 设计](#22-设计)auto - [2.3 表结构设计](#23-表结构设计)auto - [2.4 其他知识点](#24-其他知识点)auto - [3.5 文件上传](#35-文件上传)auto - [3.6 创建并设置CORS](#36-创建并设置cors)auto - [3. 其他](#3-其他)autoauto # 项目概述 ## 1. 同步运行环境 ### 1.1 建立虚拟环境,同步python版本 ``` conda create -n [环境名称] python==[版本号] ``` ### 1.2 同步项目依赖包 + 导出依赖包 ``` pip freeze > requirement.txt ``` + 安装依赖包 ``` pip install -r requirement.txt ``` ## 2. 独立配置文件 + 在settings.py同级目录下建立local_settings.py文件,并将本地配置信息写入local_settings.py文件 + 在settings.py文件末尾添加如下代码: ```python try: from .local_settings import * except ImportError: pass ``` > 即表示用local_settings.py文件中配置信息覆盖settings.py,若为空则pass ## 3. 腾讯云平台 + sms短信,申请服务 + cos对象存储,腾讯给你云硬盘,用于项目文件上传/下载/查看 ## 4. redis + 远程字典服务 + 数据存在内存里,要考虑内存大小 + 键值对型数据 + 通过 redis==3.4.1 模块操作 + 内存操作比硬盘(MySQL)快 + 可以设置数据超时时间,超时自动删除 ## 5. 项目开发分期 + 一期: 用户认证 (短信验证,图片验证码,django ModelForm组件) + 二期: wiki,文件,问题管理 + 三期: 支付,部署 # 项目开发 ## 1. 前戏 ### 1.1 项目虚拟环境 + 使用 virtualenv 包管理虚拟环境 + 安装 virtualenv 模块: ``` pip install virtualenv -ihttps://pypi.douban.com/simple ``` + 创建虚拟环境: ``` virtualenv [环境名称] ``` > 会在当前目录创建[环境名称]文件夹,建议单独创建一个目录 + 激活虚拟环境: ``` source s25/bin/activate ``` > 和conda activate可互相覆盖 + 退出虚拟环境: ``` deactivate ``` + 搭建django环境 + 安装django包: ``` pip install django==1.11.28 -ihttps://pypi.douban.com/simple ``` + 创建django项目: ``` django_admin startproject s25 ``` + 创建app: ``` cd s25 python manage.py startapp app01 ``` > 注意: 一定要创建虚拟开发环境,避免环境冲突 ### 1.2 项目框架 + 重写local_settings,设置本地配置文件: ```python DEBUG = True ALLOWED_HOSTS = ['*',] INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'app01', ] LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_TZ = False SMS = 666 ``` > 给别人代码时不要把local_settings.py文件给他 ### 1.3 git实战应用 #### 1.3.1 创建远程代码仓库 + 使用gitee创建仓库 #### 1.3.2 本都代码推送到远程仓库 + 在项目根目录下创建文件 .gitignore 用于git上传时忽略部分文件 ``` # pycharm .idea/ .DS_Store __pycache__/ *.py[cod] *$py.class # Django stuff: local_settings.py *.sqlite3 # database migrations */migrations/*.py !*/migrations/__init__.py ``` + 创建本地git: ``` git init git add . git commit -m "new project store" ``` + git 本地项目推送到远程仓库 ``` (s28) [root@VM-0-4-centos s25]# git push https://gitee.com/energylu/s25.git master Counting objects: 17, done. Compressing objects: 100% (15/15), done. Writing objects: 100% (17/17), 5.27 KiB | 0 bytes/s, done. Total 17 (delta 0), reused 0 (delta 0) remote: Powered by GITEE.COM [GNK-5.0] To https://gitee.com/energylu/s25.git * [new branch] master -> master ``` + 测试获取代码: ``` git clone [git地址] ``` + 导出项目依赖包: 导出包: ``` pip freeze > requirement.txt ``` 导入包 ``` pip install -r requirement.txt ``` ### 1.4 通过python结合腾讯sms发送短信 + 项目中应用: + 注册 + 登录 + 腾讯云平台>开通云短信 https://pythonav.com/wiki/detail/10/81/ + 安装SDK ``` pip install qcloudsms_py ``` + 代码拿来项目中用 新建一个 utils/tencent 目录,在目录下新建 sms.py 文件,把代码粘进去 + 把appid,key等信息改到settings或local_settings里 > from django.conf import settings + 编写路由和视图函数,实现发送短信 ### 1.5 Django的ModleForm组件 #### 1.5.1 ModleForm应用场景 + 自动生成标签 + 实现表单认证 #### 1.5.2 实现过程 + 配置路由,编写视图函数 + 在app01目录下新建templates目录,编写register.html文件 + 在视图函数中加入ModelForm配置,并调整视图视图函数 ```python from django import forms from app01 import models from django.core.validators import RegexValidator from django.core.exceptions import ValidationError class RegisterModelForm(forms.ModelForm): # 手机号正则验证 mobile_phone = forms.CharField(label='手机号', validators=[RegexValidator(r'^(1[3|4|5|6|7|8|9])\d{9}$', '手机号格式错误'), ]) # 将密码改为密文显示 pass_word = forms.CharField(label='密码',widget=forms.PasswordInput()) # 确认密码 confirm_password = forms.CharField(label='重复密码',widget=forms.PasswordInput()) # 增加验证码字段 code = forms.CharField(label='验证码',widget=forms.TextInput()) class Meta(): model = models.UserInfo fields = "__all__" def register(request): form = RegisterModelForm() return render(request, 'register.html', {'form': form}) ``` > ModelForm可以修改model中定义的字段,也可以新增字段(确认密码,验证码) #### 1.5.3 页面美化 + 到 https://www.bootcdn.cn/twitter-bootstrap/ 页面去找个css样式 + 使用3.4.1 https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap-theme.min.css + 在register.html里加入对应设置 + 重写 RegisterModelForm 的 __init__ 方法: ```python def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for name, field in self.fields.items(): field.widget.attrs['class'] = 'form-control' field.widget.attrs['placeholder'] = '请输入%s' % (field.label,) ``` + html中,验证码按钮实现 ```html {% if field.name == 'code' %}
{{ field }}
{% else %}
{{ field }}
{% endif %} ``` + 修改meta中的fields属性,重排各字段显示顺序,把验证码和手机号放一起 ```python class Meta(): model = models.UserInfo fields = ['username', 'email', 'pass_word', 'confirm_password', 'mobile_phone', 'code'] ``` #### 1.5.4 注册实现 + 点击获取验证码 + 获取手机号 + 向后台发送ajax + 手机 + tpl=register + 向手机发送验证码 + 验证码时效60s(redis) ### 1.6 redis #### 1.6.1 安装redis软件 > https://pythonav.com/wiki/detail/10/82/ > https://www.cnblogs.com/happywish/p/10944253.html > /usr/local/redis/bin/redis-server /usr/local/redis/etc/redis.conf #### 1.6.2 安装redis模块 + 安装模块: ``` pip install redis ``` + 代码连接: ```python #!/usr/bin/env python # -*- coding:utf-8 -*- import redis # 直接连接redis conn = redis.Redis(host='10.211.55.28', port=6379, password='foobared', encoding='utf-8') # 设置键值:15131255089="9999" 且超时时间为10秒(值写入到redis时会自动转字符串) conn.set('15131255089', 9999, ex=10) # 根据键获取值:如果存在获取值(获取到的是字节类型);不存在则返回None value = conn.get('15131255089') print(value) ``` + 连接池连接(推荐): ```python import redis # 创建redis连接池(默认连接池最大连接数 2**31=2147483648) pool = redis.ConnectionPool(host='10.211.55.28', port=6379, password='foobared', encoding='utf-8', max_connections=1000) # 去连接池中获取一个连接 conn = redis.Redis(connection_pool=pool) # 设置键值:15131255089="9999" 且超时时间为10秒(值写入到redis时会自动转字符串) conn.set('name', "武沛齐", ex=10) # 根据键获取值:如果存在获取值(获取到的是字节类型);不存在则返回None value = conn.get('name') print(value) ``` #### 1.6.3 Django-redis模块 在django中方便使用redis ``` pip install django-redis==4.11.0 -ihttps://pypi.douban.com/simple ``` 使用下列代码连接redis: ```python # local_settings 里加入配置 # 上面是django项目settings中的其他配置.... CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://10.211.55.28:6379", # 安装redis的主机的 IP 和 端口 "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "CONNECTION_POOL_KWARGS": { "max_connections": 1000, "encoding": 'utf-8' }, "PASSWORD": "foobared" # redis密码 } } } # 在django的视图中操作redis from django.shortcuts import HttpResponse from django_redis import get_redis_connection def index(request): # 去连接池中获取一个连接 conn = get_redis_connection("default") conn.set('nickname', "武沛齐", ex=10) value = conn.get('nickname') print(value) return HttpResponse("OK") ``` ## 2. 实现注册 ### 2.1 展示注册页面 #### 2.1.1 创建web应用 + 把views.py文件改为views目录 #### 2.1.2 创建模板文件目录 + 模板文件路径处理 #### 2.1.3 母版准备 + 创建静态文件 + 本地静态文件 + bootstrap -- 3.3.7 (css,js线上框架) + FontAwesome -- 4.7.0 (图标) + 模板->头部菜单,可以直接到bootstrap里找 > basic.html 头部要加上 {% load static %}, 否则静态文件无法加载 + 导航条处理 + 注册页面处理 #### 2.1.4 url准备 ```python # 根路由 from django.conf.urls import url,include from django.contrib import admin urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^app01/', include('app01.urls',namespace='app01')), url(r'^/', include('web.urls')), ] # app01.urls from django.conf.urls import url from app01 import views urlpatterns = [ url(r'^send/sms/', views.send_sms), url(r'^register/', views.register), url(r'^login/', views.login), ] # web.urls from django.conf.urls import url from web.views import account urlpatterns = [ url(r'^register/', account.register, name='account'), ] ``` > namespace用于反向解析区分同名路由,django2.2以后在urls.py文件中加上app_name='app01' #### 2.1.5 form表单 + 在web目录下新建form目录,并写入代码: ```python from django import forms # from app01 import models from web import models from django.core.validators import RegexValidator from django.core.exceptions import ValidationError class RegisterModelForm(forms.ModelForm): mobile_phone = forms.CharField(label='手机号', validators=[RegexValidator(r'^(1[3|4|5|6|7|8|9])\d{9}$', '手机号格式错误'), ]) pass_word = forms.CharField(label='密码',widget=forms.PasswordInput()) confirm_password = forms.CharField(label='重复密码',widget=forms.PasswordInput()) code = forms.CharField(label='验证码',widget=forms.TextInput()) class Meta(): model = models.UserInfo fields = ['username', 'email', 'pass_word', 'confirm_password', 'mobile_phone', 'code'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for name, field in self.fields.items(): field.widget.attrs['class'] = 'form-control' field.widget.attrs['placeholder'] = '请输入%s' % (field.label,) ``` + 在views/account.py写入代码: ```python from web.forms.account import RegisterModelForm def register(request): form = RegisterModelForm() return render(request, 'register.html', {'form': form}) ``` + 把app01/models.py中UserInfo迁移过来,并修改forms/account.py中引用 ### 2.2 点击获取验证码 #### 2.2.1 绑定点击事件 ```js // 页面框架加载完成之后自动执行函数 $(function () { bindClickBtnSms();/*发送验证码*/ bindClickSubmit();/*注册*/ }); ``` #### 2.2.2 获取手机号 ```js // 获取用户输入的手机号 // 找到输入框的ID,根据ID获取值,如何找到手机号的那个ID? var mobilePhone = $('#id_mobile_phone').val(); ``` #### 2.2.3 发送ajax请求 ```js // 发送ajax请求,把手机号发送过去 $.ajax({ url: "{% url 'send_sms' %}", // 等价于 /send/sms/ type: "GET", data: {mobile_phone: mobilePhone, tpl: "register"}, success: function (res) { console.log(res) } }) ``` #### 2.2.4 手机号格式校验 + 不能为空,格式正确 在forms/account.py中添加SendSmsForm类校验: ```python class SendSmsForm(forms.Form): mobile_phone = forms.CharField(label='手机号', validators=[RegexValidator(r'^(1[3|4|5|6|7|8|9])\d{9}$', '手机号格式错误'), ]) ``` 填充send_sms视图函数 ```python from web.forms.account import SendSmsForm def send_sms(request): """发送短信""" print(request.GET) mobile_phone = request.GET.get('mobile_phone') tpl = request.GET.get('tpl') form = SendSmsForm(data=request.GET) # 只是校验手机号不为空或格式校验 if form.is_valid(): pass return HttpResponse('成功') ``` + 未注册 在SendSmsForm类中添加自定义校验: ```python class SendSmsForm(forms.Form): mobile_phone = forms.CharField(label='手机号', validators=[RegexValidator(r'^(1[3|4|5|6|7|8|9])\d{9}$', '手机号格式错误'), ]) def clean_mobile_phone(self): mobile_phone = self.cleaned_data['mobile_phone'] # 校验手机号在数据库中是否存在 exists = models.UserInfo.objects.filter(mobile_phone=mobile_phone).exists() if exists: raise ValidationError('手机号已存在') return mobile_phone ``` + tpl校验(校验尽量都放到高等函数里) ```python # views/account.py def send_sms(request): """发送短信""" print(request.GET) mobile_phone = request.GET.get('mobile_phone') tpl = request.GET.get('tpl') form = SendSmsForm(request, data=request.GET) # 把request传给SendSmsForm # 只是校验手机号不为空或格式校验 if form.is_valid(): pass return HttpResponse('成功') # forms/account.py class SendSmsForm(forms.Form): mobile_phone = forms.CharField(label='手机号', validators=[RegexValidator(r'^(1[3|4|5|6|7|8|9])\d{9}$', '手机号格式错误'), ]) def __init__(self,request,*args,**kwargs): super().__init__(*args,**kwargs) self.request = request def clean_mobile_phone(self): """手机号校验的钩子""" mobile_phone = self.cleaned_data['mobile_phone'] # 判断短信模板是否有问题 tpl = self.request.GET.get('tpl') template_id = settings.TENCENT_SMS_TEMPLATE.get(tpl) if not template_id: raise ValidationError('模板不存在') # 校验手机号在数据库中是否存在 exists = models.UserInfo.objects.filter(mobile_phone=mobile_phone).exists() if exists: raise ValidationError('手机号已存在') return mobile_phone ``` > 重写SendSmsForm的__init__方法,可以实现把request传到SendSmsForm类中使用高等函数校验 #### 2.2.5 验证通过 将校验方法写在钩子函数中,forms/account.py -> class SendSmsForm -> def clean_mobile_phone: ```python # 导入模块 import random from utils.tencent.sms import send_sms_single from django_redis import get_redis_connection # 钩子函数内容 # 发短信和写redis code = random.randrange(1000, 9999) # 发送短信 sms = send_sms_single(mobile_phone, template_id, [code, ]) if sms['result'] != 0: raise ValidationError("短信发送失败,{}".format(sms['errmsg'])) # 验证码写入redis(django-redis) conn = get_redis_connection() conn.set(mobile_phone, code, ex=60) ``` 完善视图函数views/account.py -> send_sms: ```python def send_sms(request): """发送短信""" print(request.GET) mobile_phone = request.GET.get('mobile_phone') tpl = request.GET.get('tpl') form = SendSmsForm(request, data=request.GET) # 把request传给SendSmsForm # 只是校验手机号不为空或格式校验 if form.is_valid(): # 发短信 # 写redis return JsonResponse({'status': True}) return JsonResponse({'status': False,'error': form.errors}) ``` + 发送短息 + 将短信保存到redis中(60s) #### 2.2.6 发送成功或失败 + 发送成功 -> 倒计时 + 发送失败 -> > dataType: "JSON";//将服务端返回的数据反序列化为字典 完善前端js,若发送成功,开始倒计时,发送失败,返回错误信息: ```js if (res.status) { sendSmsRemind(); } else { // 错误信息 // console.log(res); // {status:False, error:{ mobile_phone: ["错误信息",],code: ["错误信息",] } } $.each(res.error, function (key, value) { $("#id_" + key).next().text(value[0]); }) } } ``` sendSmsRemind(),写倒计时函数: ```js function sendSmsRemind() { var $smsBtn = $('#btnSms'); $smsBtn.prop('disabled', true); // 禁用 var time = 60; var remind = setInterval(function () { $smsBtn.val(time + '秒重新发送'); time = time - 1; if (time < 1) { clearInterval(remind); $smsBtn.val('点击获取验证码').prop('disabled', false); } }, 1000) } ``` ### 2.3 点击注册 发送ajax: ```js $.ajax({ url: '...', type: 'POST', data: {}, dataType: "JSON", success: function(res){ } }) ``` #### 2.3.1 注册按钮绑定事件 ```js /* 点击提交(注册) */ function bindClickSubmit() { $('#btnSubmit').click(function () { $('.error-msg').empty(); // 收集表单中的数据(找到每一个字段)$('#regForm').serialize() // 数据ajax发送到后台 $.ajax({ url: "{% url 'register' %}", type: "POST", data: $('#regForm').serialize(), // 所有字段数据 + csrf token dataType: "JSON", success: function (res) { if(res.status){ location.href = res.data; }else{ $.each(res.error, function (key, value) { $("#id_" + key).next().text(value[0]); }) } } }) }) } ``` > data: $('#regForm').serialize(), // 可收集字段数据 + 同时把csrf也收集了 补充register函数: ```python def register(request): if request.method == 'GET': form = RegisterModelForm() return render(request, 'register.html', {'form': form}) print(request.POST) return JsonResponse({}) ``` #### 2.3.2 数据校验 > 模型字段定义时,db_index=True,创建索引,查询加速 > 每个字段都要写校验钩子函数 #### 2.3.3 写入数据库 + 补全视图函数: ```python def register(request): if request.method == 'GET': form = RegisterModelForm() return render(request, 'register.html', {'form': form}) form = RegisterModelForm(data=request.POST) if form.is_valid(): # 验证通过,写入数据库(密码要是密文) form.save() return JsonResponse({'status': True, 'data': '/login/'}) return JsonResponse({'status': False, 'error': form.errors}) ``` + 编写注册js: ```js /* 点击提交(注册) */ function bindClickSubmit() { $('#btnSubmit').click(function () { $('.error-msg').empty(); // 收集表单中的数据(找到每一个字段)$('#regForm').serialize() // 数据ajax发送到后台 $.ajax({ url: "{% url 'register' %}", type: "POST", data: $('#regForm').serialize(), // 所有字段数据 + csrf token dataType: "JSON", success: function (res) { if(res.status){ location.href = res.data; //页面刷新跳转 }else{ $.each(res.error, function (key, value) { $("#id_" + key).next().text(value[0]); }) } } }) }) } ``` ## 3. 短信登录 ### 3.1 展示页面 + 配置路由和视图函数: ```python # web/urls.py urlpatterns = [ url(r'^login/sms/$', account.login_sms, name='login_sms'), ] # web/views/account.py def login_sms(request): if request.method == 'GET': form = LoginSMSForm() return render(request, 'login_sms.html', {'form': form}) ``` + 写login_sms.html文件: > 把register.html复制过来,把注册改为登录,把对应路由改为login_sms + 在web/forms/account.py中新建LoginSMSForm类: ```python # 把init方法封装到BootStrapForm类中,实现输入框中的样式 class LoginSMSForm(BootStrapForm,forms.Form): mobile_phone = forms.CharField(label='手机号', validators=[RegexValidator(r'^(1[3|4|5|6|7|8|9])\d{9}$', '手机号格式错误'), ]) code = forms.CharField(label='验证码',widget=forms.TextInput()) ``` ### 3.2 点击发送短信 + 绑定点击事件: 复制register.html,tpl改为的值改为login,倒计时沿用不改 > 事件仍然使用send_sms路由,需要把SendSmsForm表单验证中识别tpl,并区分登录或注册 ```python # 根据tpl区分登录和注册 exists = models.UserInfo.objects.filter(mobile_phone=mobile_phone).exists() if tpl == 'login': if not exists: raise ValidationError('手机号不存在') else: # 校验手机号在数据库中是否存在 if exists: raise ValidationError('手机号已存在') ``` ### 3.3 点击登录 + 绑定点击事件: 复制register.html,修改提交路由为login_sms + 完善login_sms视图函数,实现点击跳转 ```python def login_sms(request): if request.method == 'GET': form = LoginSMSForm() return render(request, 'login_sms.html', {'form': form}) form = LoginSMSForm(request.POST) if form.is_valid(): # 用户输入正确,登录成功 return JsonResponse({'status': True,'data': "/index/"}) return JsonResponse({'status': False, 'error': form.errors}) ``` + 提交校验 ```python mobile_phone = forms.CharField(label='手机号', validators=[RegexValidator(r'^(1[3|4|5|6|7|8|9])\d{9}$', '手机号格式错误'), ]) code = forms.CharField(label='验证码',widget=forms.TextInput()) def clean_mobile_phone(self): mobile_phone = self.cleaned_data['mobile_phone'] exists = models.UserInfo.objects.filter(mobile_phone=mobile_phone).exists() if not exists: raise ValidationError('手机号不存在') return mobile_phone def clean_code(self): """校验验证码同时校验手机号""" code = self.cleaned_data['code'] # mobile_phone = self.cleaned_data['mobile_phone'] mobile_phone = self.cleaned_data.get('mobile_phone') # 手机号不存在,验证码不需要判断 if not mobile_phone: return code conn = get_redis_connection() redis_code = conn.get(mobile_phone) if not redis_code: raise ValidationError('验证码失效或未发送,请重新发送') redis_str_code = redis_code.decode('utf-8') if code.strip() != redis_str_code: raise ValidationError('验证码错误,请重新输入') return code ``` ### 3.4 补充 + 校验mobile_phone时,把mobile_phone改为user_object,方便后续使用 ```python def clean_mobile_phone(self): mobile_phone = self.cleaned_data['mobile_phone'] user_object = models.UserInfo.objects.filter(mobile_phone=mobile_phone).first() if not user_object: raise ValidationError('手机号不存在') return user_object ``` > lean_code函数也要更改,get('mobile_phone')获取的是用户对象,不是电话号码了 ```python def clean_code(self): """校验验证码同时校验手机号""" code = self.cleaned_data['code'] # mobile_phone = self.cleaned_data['mobile_phone'] user_object = self.cleaned_data.get('mobile_phone') # 此次获取的是用户对象 # 手机号不存在,验证码不需要判断 if not user_object: return code conn = get_redis_connection() redis_code = conn.get(user_object.mobile_phone) if not redis_code: raise ValidationError('验证码失效或未发送,请重新发送') redis_str_code = redis_code.decode('utf-8') if code.strip() != redis_str_code: raise ValidationError('验证码错误,请重新输入') return code ``` + 视图函数中处理user_object,放到session中: ```python def login_sms(request): if request.method == 'GET': form = LoginSMSForm() return render(request, 'login_sms.html', {'form': form}) form = LoginSMSForm(request.POST) if form.is_valid(): # 用户输入正确,登录成功 user_object = form.cleaned_data['mobile_phone'] # 用户信息放入session,以后再实现 print(user_object) return JsonResponse({'status': True,'data': "/index/"}) return JsonResponse({'status': False, 'error': form.errors}) ``` ## 4. 用户名密码登陆 ### 4.1 python生成验证码图片 > https://www.cnblogs.com/wupeiqi/articles/5812291.html 导入pillow模块 ``` pip install pillow ``` ### 4.2 Session & Cookie ### 4.3 页面显示 + 配置路由login + 配置视图函数views/account.py 中 login + 写前端页面login.html > 改为 img 标签 > 此页面使用表单验证,加上{{ field.error.0 }} + 配置表单 forms/account.py 中 定义 LoginForm + 生成图片操作 > 字体文件放到根目录 + 验证码数据写入session > 数据存在django_session表中 + 点击验证码刷新 > 写js ```js ``` ### 4.4 登陆 + 表单认证钩子函数 ```python def clean_pass_word(self): pwd = self.cleaned_data['pass_word'] # 加密 & 返回 return encrypt.md5(pwd) def clean_code(self): """ 钩子 图片验证码是否正确? """ # 读取用户输入的yanzhengm code = self.cleaned_data['code'] # 去session获取自己的验证码 session_code = self.request.session.get('image_code') if not session_code: raise ValidationError('验证码已过期,请重新获取') if code.strip().upper() != session_code.strip().upper(): raise ValidationError('验证码输入错误') return code ``` + 视图函数实现提交登陆 ```python def login(request): """ 用户名和密码登录 """ if request.method == 'GET': form = LoginForm(request) return render(request, 'login.html', {'form': form}) form = LoginForm(request, data=request.POST) if form.is_valid(): username = form.cleaned_data['username'] pass_word = form.cleaned_data['pass_word'] # user_object = models.UserInfo.objects.filter(username=username, password=password).first() # (手机=username and pwd=pwd) or (邮箱=username and pwd=pwd) user_object = models.UserInfo.objects.filter(Q(email=username) | Q(mobile_phone=username)).filter( pass_word=pass_word).first() if user_object: # 登录成功为止1 request.session['user_id'] = user_object.id request.session.set_expiry(60 * 60 * 24 * 14) return redirect('index') form.add_error('username', '用户名或密码错误') return render(request, 'login.html', {'form': form}) ``` > Q 模块实现邮箱或手机号登陆 + 首页显示(html页面) > 用图片填充 > 导航栏,首页,登陆,注册设置 > 短信登陆,用户名密码登陆跳转 ### 4.5 登陆成功中间件 + web/middleware/auth.py写入中间件: ```python from django.utils.deprecation import MiddlewareMixin from web import models class AuthMiddleware(MiddlewareMixin): def process_request(self,request): """ 如果用户已登陆,则在request中赋值 """ user_id = request.session.get('user_id', 0) user_object = models.UserInfo.objects.filter(id=user_id).first() request.tracer = user_object ``` + 写退出登陆视图函数,并配置url和前端页面: ```python def logout(request): request.session.flush() return redirect('index') ``` + 改为连接mysql > mysql设置允许远程连接 ``` mysql> GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION; mysql> FLUSH PRIVILEGES; ``` > local_settings.py文件中数据库配置 # 二期 ## 1. 管理中心概述 ### 1.1 django离线脚本 + django: web框架 + 离线: 非web运行时 + 脚本: 一个或一组py文件 作用: 在某个py文件中对django项目做一些操作 > web未启动时,django中的模块无法直接使用,如需使用,需要做一些处理 示例1:使用离线脚本在用户表插入数据 ```python # 导入 # 模拟django环境 import os import sys import django base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 设置根路径 sys.path.append(base_dir) os.environ.setdefault('DJANGO_SETTINGS_MODULE','s25.settings') # 把项目添加到环境变量 django.setup() # 模拟启动django ``` 示例2:数据库录入全国省市县(批量数据) 示例3:朋友圈项目,敏感字识别 示例4:免费版设置:1G文件,5个项目,10人参与 ### 1.2 探讨业务 #### 1.2.1 价格策略,建表 新用户注册拥有免费额度。 #### 1.2.2 用户 如何与价格策略关联 #### 1.2.3 交易表 id,状态,用户,价格.... #### 1.2.4 创建存储 基于腾讯对象存储COS存储数据 #### 1.2.5 项目 id,项目名称,描述,颜色,是否星标,参与人数,创建者,已使用空间 #### 1.2.6 项目参与者 id,项目id,用户id,是否星标 ### 1.3 设计表结构 + models.py文件中添加,然后数据库迁移 + 创建离线脚本,在价格策略表中添加个人免费版 + 把导入django模拟环境代码写到base.py文件中,其他脚本直接import base + 判断一下表中免费用户是否存在,不存在则添加 + 修改用户注册表,创建用户同时创建交易记录,默认购买免费版 ```python def register(request): if request.method == 'GET': form = RegisterModelForm() return render(request, 'register.html', {'form': form}) form = RegisterModelForm(data=request.POST) if form.is_valid(): # 验证通过,写入数据库(密码要是密文) instance = form.save() # 创建交易记录 policy_object = models.PricePolicy.objects.filter(category=1, title='个人免费版').first() models.Transaction.objects.create( status=2, order=str(uuid.uuid4()), user=instance, price=0, price_policy=policy_object, count=0, start_datetime=datetime.datetime.now() ) return JsonResponse({'status': True, 'data': '/login/sms/'}) return JsonResponse({'status': False, 'error': form.errors}) ``` ### 1.4 添加项目 #### 1.4.1 项目列表母版+样式 > https://v3.bootcss.com/components/ bootcss文档 + 配置urls > project/list + 编写视图函数 views/project.py project_list + 编写项目管理母版 layout/manage.html + 编写项目列表页面 project_list.html + 中间件实现判断是否登录,未登录自动跳转至登录页面(添加白名单) + 获取当前登录用户的额度 > 可以在用户表中加个付费策略字段,查询加速 > 把user,object,price_policy封装到Tracer,记得把前端页面tracer.usernam改为tracer.user.username #### 1.4.2 添加项目 + 添加按钮绑定事件 + 创建对话框实现 + 创建项目form表单 forms/project.py ProjectModelForm类, fields显示字段 + 创建项目提交js + 视图函数完成创建项目存入数据库,creator字段要赋值 + 表单验证钩子函数 + 前端页面和js,钩子函数,对照 + 添加项目测试 #### 1.4.3 查看项目列表 + 从数据库中获取数据: + 我创建的项目 + 我参与的项目 + 提取星标项目 + 修改视图函数get请求相关代码: ```python if request.method == 'GET': # GET请求查看项目列表 """ 1. 从数据库中获取两部分数据 我创建的所有项目:已星标、未星标 我参与的所有项目:已星标、未星标 2. 提取已星标 列表 = 循环 [我创建的所有项目] + [我参与的所有项目] 把已星标的数据提取 得到三个列表:星标、创建、参与 """ project_dict = {'star': [], 'my': [], 'join': []} # 创建字典 my_project_list = models.Project.objects.filter(creator=request.tracer.user) for row in my_project_list: # 遍历后分别添加到star或my if row.star: project_dict['star'].append(row) else: project_dict['my'].append(row) join_project_list = models.ProjectUser.objects.filter(user=request.tracer.user) for item in join_project_list: # 遍历后分别添加到star或join if item.star: project_dict['star'].append(item.project) else: project_dict['join'].append(item.project) form = ProjectModelForm(request) return render(request, 'project_list.html', locals()) ``` + 在前端页面中添加项目展示框 ```html
星标
{% for item in project_dict.star %}
{{ item.name }}
{% endfor %}
``` > 踩坑: html多行注释中,如果有无法解析的路由,也会导致页面解析错误,不知道啥原因 + 样式设置,调整前端页面代码: ```html
{% for item in project_dict.star %}
{{ item.name }}
{{ item.creator.username }}
{{ item.join_count }}
{% endfor %}
``` + 编写css样式代码 > 踩坑: html解析报错,改为wxml正常 #### 1.4.4 星标 + 配置添加/移除星标路由, ```python urlpatterns = [ # /project/star/my/1 # /project/star/join/1 # 需要传入参数,project_type: my/join/star,project_id:1/2/3 url(r'^project/star/(?P\w+)/(?P\d+)/$', project.project_star, name='project_star'), url(r'^project/unstar/(?P\w+)/(?P\d+)/$', project.project_unstar, name='project_unstar'), ] ``` + 编写对应视图函数 ```python def project_star(request, project_type, project_id): """ 星标项目 """ if project_type == 'my': models.Project.objects.filter(id=project_id, creator=request.tracer.user).update(star=True) return redirect('project_list') if project_type == 'join': models.ProjectUser.objects.filter(project_id=project_id, user=request.tracer.user).update(star=True) return redirect('project_list') return HttpResponse('请求错误') def project_unstar(request, project_type, project_id): """ 取消星标 """ if project_type == 'my': models.Project.objects.filter(id=project_id, creator=request.tracer.user).update(star=False) return redirect('project_list') if project_type == 'join': models.ProjectUser.objects.filter(project_id=project_id, user=request.tracer.user).update(star=False) return redirect('project_list') return HttpResponse('请求错误') ``` + 前端页面中绑定点击事件 + 为移除星标,需要在后台中,星标字典中,加入type以区分join和my,同时前端页面引用都需要修改 > 踩坑: 前端页面,只有星标模块需要加value,my和join模块不需要加 ### 1.5 功能实现 + 项目颜色选择 + 修改Bootstrap类,设置部分应用(面向对象),web/forms/bootstrap.py ```python class BootStrapForm(object): BOOTSTRAP_CLASS_EXCLUDE = [] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for name, field in self.fields.items(): if name in self.BOOTSTRAP_CLASS_EXCLUDE: continue old_class = field.widget.attrs.get('class', "") field.widget.attrs['class'] = '{} form-control'.format(old_class) field.widget.attrs['placeholder'] = '请输入%s' % (field.label,) ``` + 自定义ModelForm插件,web/forms/widgets.py ```python class ColorRadioSelect(RadioSelect): # template_name = 'django/forms/widgets/radio.html' # option_template_name = 'django/forms/widgets/radio_option.html' template_name = 'widgets/color_radio/radio.html' option_template_name = 'widgets/color_radio/radio_option.html' ``` + 重写对应母版,添加标签,web/templates/widgets + 前端页面写css样式代码 ```css .color-radio label { margin-left: 0; padding-left: 0; } .color-radio input[type="radio"] { display: none; } .color-radio input[type="radio"] + .cycle { display: inline-block; height: 25px; width: 25px; border-radius: 50%; border: 2px solid #dddddd; } .color-radio input[type="radio"]:checked + .cycle { border: 2px solid black; } ``` + 自定义插件中设置样式,web/forms/project.py ```python class ProjectModelForm(BootStrapForm,forms.ModelForm): BOOTSTRAP_CLASS_EXCLUDE = ['color',] # 继承时改写此参数 class Meta: model = models.Project fields = ['name', 'color', 'desc'] widgets = { 'desc': forms.Textarea(attrs={'xx':123}), # 'xx':123表示设置属性 'color': ColorRadioSelect(attrs={'class': 'color-radio'}) } ``` + 下拉项目列表 + inclusion_tag: web/templatetags 目录下py文件用于导入templates/inclusion 目录下前端模板导入到前端页面作为一个代码块 + web/templatetags 目录下,新建project.py文件: ```python from django.template import Library register = Library() @register.inclusion_tag('inclusion/all_project_list.html') # templates里找html文件,然后完成渲染 def all_project_list(): return {'name': 'jack'} ``` + templates/inclusion 目录下,新建all_project_list.html文件: ```html
safsaf: {{ name }}
``` + 母版 manage.html 中写入: ```html {% load project %} ...