# trance
**Repository Path**: wljess/trance
## Basic Information
- **Project Name**: trance
- **Description**: bug轻量级管理仓库
- **Primary Language**: Python
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2020-06-28
- **Last Updated**: 2020-12-19
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 一、项目环境搭建
## 1.1 虚拟环境搭建
创建虚拟环境
- 利用python自带venv工具进行创建虚拟环境
```python
#在工作目录下执行如下操作
1.python -m venv .venv(虚拟环境名称或路径)
2.cd .venv/Scripts/
#激活虚拟环境
3.activate
#激活虚拟环境后切记退回工作目录
4.cd ../../
#退出虚拟环境
5.deactivate
```
- 利用virtualvenv创建虚拟环境
```python
pip install virtualvenv
#创建虚拟环境
virtualvenv myenv(虚拟环境名称或路径)
#进入虚拟环境(win是Scripts,Linux:source bin/activate)
cd myenv/Scripts
#激活虚拟环境
activate
#退出虚拟环境
deactivate
#删除虚拟环境,直接删除文件夹即可
```
- 利用virtualenvwrapper创建虚拟环境
```python
pip install virtualenvwrapper
#列出虚拟环境列表:
workon
#新建虚拟环境:
mkvirtualenv[虚拟环境名称]
#启动/切换虚拟环境:
workon[虚拟环境名称]
#离开虚拟环境:
deactivate
```
- 利用pycharm创建虚拟环境

## 1.2 django环境搭建
- 安装django
```python
#进入虚拟环境工作目录,安装django
pip install django == 版本号
```
- 创建django项目文件
```python
django-admin startproject project_name .
```
- 创建django应用文件
```python
python manage.py startapp app_name
```
- 路由配置
```python
#主URL配置
from django.contrib import admin
from django.urls import path,include
urlpatterns = [
path('admin/', admin.site.urls),
path('app/',include('App.urls',namespace='app')),
path('web/',include('Web.urls',namespace='web'))
]
```
```python
#app中url配置
from django.urls import path
from . import views
app_name = 'app'
urlpatterns = [
path('',views.hello,name='hello'),
path('send/sms/',views.send_msg,name='send_msg'),
path('register/',views.register,name='register')
]
```
- 启动django项目进行简单测试
```python
python manage.py runserver 端口号
#若项目能正常启动
#在页面打开对应url,看是否能正常在页面显示hello word
from django.shortcuts import render
from django.http import HttpResponse
def hello(request):
return HttpResponse('hello word!!!')
```
## 1.3 本地local_settings.py文件配置
```python
#在主项目(project_name)目录下新建local_settings.py
#背景:实际开发过程中测试开发所连接数据库及settings.py文件不一致
#在settings.py文件末尾加上如下配置:
#settings.py末尾加上
try:
#要把整个文件里面内容导出而不是只导入该文件
#from myapp import local_settings
from .local_settings import *
except ImportError:
pass
#local_settings.py文件中进行差异化配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': '数据库名称',
'USER':'用户名',
'PASSWORD':'密码',
'PORT':'3306',
'HOST':'0.0.0.0'
}
}
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
STATIC_FILES = os.path.join(BASE_DIR,'static')
MEDIA_ROOT = os.path.join(BASE_DIR,'static/upload/')
```
## 1.4 git代码提交远程仓库
- 远程代码仓库创建申请
登录码云代码托管中心:https://gitee.com/
- 注册

- 登录码云新建仓库


- gitignore文件配置
```git
# VScod
.idea/
.vscode/
.DS_Store
.venv/
__pycache__/
*.py[cod]
*$py.class
# Django stuff:
local_settings.py
*.sqlite3
# database migrations
*/migrations/*.py
!*/migrations/__init__.py
```
- 设置签名
系统用户级别:登录当前操作系统的用户范围
- git config --global user.name username
- git config --global user.email useremail
- 信息保存位置:

- 创建本地仓库推送代码到远程仓库
```git
# 也可以直接在项目目录进行初始化提交远程仓库即可
mkdir djangoDemo01
cd djangoDemo01
git init
touch README.md
git add README.md
git commit -m "first commit"
git remote add origin https://gitee.com/wljess/djangoDemo01.git
git push -u origin master
```
- git常用指令
```git
#实际开发中流程
#创建本地代码托管目录
mkdir codeStore
#进入本地代码仓库
cd codeStore
#初始化本地仓库
git init
#在编辑器中将代码修改完毕后,查看当前修改代码状态
git status
#将修改的代码提交到暂存区
git add .
#提交完毕之后再git status确保添加到暂存区的文件确实为需要提交的代码
git status
#若发现某个文件或者文件不需要提交到远程仓库
git rm -r --cache 目录
git rm --cache 文件
#提交并加注释
git commit -m "注释内容"
#查看所有分支
git branch -a
#切换到自己要提交的分支
git checkout -b 要提交代码的分支
#将远程代码仓库地址添加到本地
git remote add origin https://gitee.com/wljess/djangoDemo01.git
#将本地代码推送到远程仓库
git push -u origin 分支名
#代码冲突:
1.如果发现提交错误,可以回退之前的版本(简单粗爆)
git reflog
git reset --hard 索引值
2.分析检测代码提交冲突原因(一般原因为多个人对同一个文件同一个函数进行修改)
找对应开发进行沟通商量,确定解决方案,删除分歧代码重新提交冲突文件即可
```
## 1.5 生成requirments.txt文件
```python
pip freeze > requirements.txt
#测试的兄弟可以通过输入该指令安装项目所需模块
pip install -r requirements.txt
```
# 二、项目储备知识
## 2.1 注册腾讯云&&开通云短信
参考链接: https://pythonav.com/wiki/detail/10/81/
- 腾讯云短信/阿里云短信:(阅读产品文档:文档不清晰:谷歌、必应、搜狗)
- API:提供URL,你根据这些URL并根据提示传参数
- SDK:模块;下载安装模块,基于模块完成功能。
## 2.2 使用Python发送短信
- 项目目录结构

- 安装SDK
```python
pip install qcloudsms_py
```
- 基于SDK发送短信
```python
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import ssl
# ssl._create_default_https_context = ssl._create_unverified_context
from qcloudsms_py import SmsMultiSender, SmsSingleSender
from qcloudsms_py.httpclient import HTTPError
def send_sms_single(phone_num, template_id, template_param_list):
"""
单条发送短信
:param phone_num: 手机号
:param template_id: 腾讯云短信模板ID
:param template_param_list: 短信模板所需参数列表,例如:【验证码:{1},描述:{2}】,则传递参数 [888,666]按顺序去格式化模板
:return:
"""
appid = 112142311 # 自己应用ID
appkey = "8cc5b87123y423423412387930004" # 自己应用Key
sms_sign = "Python之路" # 自己腾讯云创建签名时填写的签名内容(使用公众号的话这个值一般是公众号全称或简称)
sender = SmsSingleSender(appid, appkey)
try:
response = sender.send_with_param(86, phone_num, template_id, template_param_list, sign=sms_sign)
except HTTPError as e:
response = {'result': 1000, 'errmsg': "网络异常发送失败"}
return response
def send_sms_multi(phone_num_list, template_id, param_list):
"""
批量发送短信
:param phone_num_list:手机号列表
:param template_id:腾讯云短信模板ID
:param param_list:短信模板所需参数列表,例如:【验证码:{1},描述:{2}】,则传递参数 [888,666]按顺序去格式化模板
:return:
"""
appid = 112142311
appkey = "8cc5b87123y423423412387930004"
sms_sign = "Python之路"
sender = SmsMultiSender(appid, appkey)
try:
response = sender.send_with_param(86, phone_num_list, template_id, param_list, sign=sms_sign)
except HTTPError as e:
response = {'result': 1000, 'errmsg': "网络异常发送失败"}
return response
if __name__ == '__main__':
result1 = send_sms_single("15131255089", 548760, [666, ])
print(result1)
result2 = send_sms_single( ["15131255089", "15131255089", "15131255089", ],548760, [999, ])
print(result2)
```
## 2.3 ModeForm生成注册字段
- 功能:1.自动生成表单2.进行表单验证
```python
# models.py文件中定义UserInfo用户类
from django.db import models
class UserInfo(models.Model):
username = models.CharField(verbose_name="用户名",max_length=32)
email = models.CharField(verbose_name="邮箱",max_length=32)
password = models.CharField(verbose_name="密码",max_length=15)
mobile_phone = models.CharField(verbose_name="手机号",max_length=15)
# 执行数据迁移
python manage.py makemigrations app_name
python manage.py migrate
```
```python
# views.py中定义register方法
from django.shortcuts import render
from django.http import HttpResponse
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}$', '手机号格式错误'), ])
password = forms.CharField(label='密码',widget=forms.PasswordInput())
confirm_password = forms.CharField(label='确认密码',widget=forms.PasswordInput())
code = forms.CharField(label='验证码')
class Meta:
model = models.UserInfo
fields = "__all__"
def register(request):
form = RegisterModelForm()
return render(request,'register.html',{"form":form})
```
```html
注册页面
注册
{% for filed in form %}
{{ filed.label }}:{{ filed }}
{% endfor %}
```
自动生成的表单页面呈现如下:

## 2.4 ModeForm美化页面
由于利用原始的ModeForm表单自带的样式比较丑,所以需要用到bootstrap框架对ModelForm表单进行美化。
boocdn网址: https://www.bootcdn.cn/
bootcdn官网上下载对应的:css、js、JQuery文件引入到需要渲染的HTML文件,建议使用min压缩版本。

bootstrap官网: https://v3.bootcss.com/
bootstrap官网下载对应插件,用于快速搭建前端页面。

```html
注册页面
```
```python
# views.py文件
from django.shortcuts import render
from django.http import HttpResponse
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}$', '手机号格式错误'), ])
# password = forms.CharField(label='密码',
# widget=forms.PasswordInput(attrs={'class':"form-control",'placeholder':"请输入密码"}))
password = forms.CharField(label='密码',
widget=forms.PasswordInput())
confirm_password = forms.CharField(label='确认密码',
widget=forms.PasswordInput())
code = forms.CharField(label='验证码')
class Meta:
model = models.UserInfo
#fields = "__all__"
fields = ['username','password','confirm_password','email','mobile_phone','code']
def __init__(self,*args,**kwargs):
super().__init__(*args,**kwargs)
for name,filed in self.fields.items():
filed.widget.attrs['class'] = "form-control"
filed.widget.attrs['placeholder'] = '请输入%s'%(filed.label,)
def register(request):
form = RegisterModelForm()
return render(request,'register.html',{"form":form})
```
## 2.5 redis基本操作
### 2.5.1 安装redis
安装指导: https://pythonav.com/wiki/detail/10/82/
### 2.5.2 python操作redis模块
- pip install redis
- 启动redis服务端
- 启动redis客户端
- python连接并操作redis
```python
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操作redis的示例是以直接创建连接的方式实现,每次操作redis如果都重新连接一次效率会比较低,建议使用redis连接池来替换,例如:
```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)
```
## 2.6 django连接redis
```python
# django中views.py文件
import redis
from django.shortcuts import HttpResponse
# 创建redis连接池
POOL = redis.ConnectionPool(host='10.211.55.28', port=6379, password='foobared', encoding='utf-8', max_connections=1000)
def index(request):
# 去连接池中获取一个连接
conn = redis.Redis(connection_pool=POOL)
conn.set('name', "武沛齐", ex=10)
value = conn.get('name')
print(value)
return HttpResponse("ok")
```
上述可以实现在django中操作redis。**但是**,这种形式有点非主流,因为在django中一般不这么干,而是用另一种更加简便的的方式。
**第一步**:安装django-redis模块(内部依赖redis模块)
```python
pip3 install django-redis
```
**第二步**:在django项目的settings.py中添加相关配置
```python
# 上面是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
```python
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")
```
# 三、注册
## 3.1 注册思路
- 点击获取验证码
- 获取手机号
- 向后台发送ajax请求
- 向手机发送验证码(ajax请求/SMS短信接口发送短信/利用redis存储验证码:手机号:验证码键值对,失效时间60秒)
- 验证码失效处理60S
## 3.2 实现注册
### 3.2.1 展示注册页面
#### 3.2.1.1 创建web应用&注册
```python
# 创建web应用
python manage.py startapp web
```
```python
# 主urls.py文件中配置
# from django.conf.urls import url
from django.contrib import admin
from django.urls import path,include
urlpatterns = [
path('admin/', admin.site.urls),
path('app/',include('app01.urls',namespace='app')),
path('wen/',include('web.urls',namespace='web'))
]
```
```python
# web应用app中配置
from django.urls import path
from . import views
app_name = 'web'
urlpatterns = [
path('',views.hello,name='hello'),
]
```
```python
# local_settings.py文件中注册web应用
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app01',
'web'
]
```
```python
# views.py文件hello测试方法
from django.http import HttpResponse
# Create your views here.
def hello(request):
return HttpResponse('hello word!!!')
# 启动django项目,测试web应用是否生效
python manage.py runserver
```
#### 3.2.1.2 多app模板处理

#### 3.2.1.3 母版装备
图标网址: https://fontawesome.dashgame.com/
bootstrap(CSS/JS/JQuery静态资源文件下载): https://v3.bootcss.com/

项目开发中部分页面之间存在大部分雷同,所以可提取公共模板,在其它相似页面中引入即可。
```html
{% block title %}{% endblock %}
{% block css %}{% endblock %}
{% block content %}{% endblock %}
{% block js %}{% endblock %}
```
```html
{% extends 'layout/basic.html' %}
{% block title %}用户注册{% endblock %}
{% block css %}{% endblock %}
{% block content %}{% endblock %}
{% block js %}{% endblock %}
```
#### 3.2.1.4 路由分配
为了方便路由管理和项目的层次结构,可以在主urls.py中引入对应项目app,在各自app应用目录下新建urls.py文件适配相关路由。
```python
# 主urls.py文件
from django.contrib import admin
from django.urls import path,include
urlpatterns = [
path('admin/', admin.site.urls),
path('app/',include('app01.urls',namespace='app')),
path('web/',include('web.urls',namespace='web'))
]
```
```Python
# app01应用下urls.py文件
from django.urls import path
from . import views
app_name = 'app'
urlpatterns = [
path('',views.hello,name='hello'),
path('send/sms/',views.send_msg,name='send_msg'),
path('register/',views.register,name='register')
]
```
```python
# web应用下urls.py文件
from django.urls import path
from web.views import account
app_name = 'web'
urlpatterns = [
path('',account.hello,name='hello'),
path('register/',account.register,name='register')
]
```
#### 3.2.1.5 导航条处理

```html
```
### 3.2.2 点击获取验证码
#### 3.2.2.1 按钮绑定点击事件
```html
```
```javascript
/*可以根据点击获取验证码的元素获取该标签并为该标签绑定点击事件*/
// 页面框架加载完成之后自动执行函数
$(function(){
// 按钮绑定事件
bindClickBtnSms();
});
/*点击获取验证码按钮绑定事件*/
function() bindClickBtnSms{
$('#btnSms').click(function() {
})
};
```
#### 3.2.2.2 获取手机号&向后端发送ajax请求获取手机号
```javascript
/*点击获取验证码按钮绑定事件*/
function() bindClickBtnSms{
$('#btnSms').click(function() {
//获取用户输入的手机号,根据手机号字段id值,获取手机号
var mobilePhone = $('#id_mobile_phone').val();
// 发送ajax请求,把手机号发送给后端
$.ajax({
"url":"",
"type":"",
"data":{},
"dataType":"JSON",
"success":fuction(res){
if (res.status) {
// 短信发送成功的业务逻辑
} else {
// 短信发送失败,前端页面输出错误信息
}
}
})
})
};
```
#### 3.2.2.3 手机号校验
django中的Form表单类实现对手机号的校验。

```python
# forms目录下account.py文件
import random
from django import forms
from app01 import models
from django.core.validators import RegexValidator
from django.core.exceptions import ValidationError
from trance import local_settings
from utils.tencent.sms import send_sms_single
from django_redis import get_redis_connection
class RegisterModelForm(forms.ModelForm):
mobile_phone = forms.CharField(label='手机号', validators=[RegexValidator(r'^(1[3|4|5|6|7|8|9])\d{9}$', '手机号格式错误'), ])
# password = forms.CharField(label='密码',
# widget=forms.PasswordInput(attrs={'class':"form-control",'placeholder':"请输入密码"}))
password = forms.CharField(label='密码',
widget=forms.PasswordInput())
confirm_password = forms.CharField(label='确认密码',
widget=forms.PasswordInput())
code = forms.CharField(label='验证码')
class Meta:
model = models.UserInfo
#fields = "__all__"
fields = ['username','password','confirm_password','email','mobile_phone','code']
def __init__(self,*args,**kwargs):
super().__init__(*args,**kwargs)
for name,filed in self.fields.items():
filed.widget.attrs['class'] = "form-control"
filed.widget.attrs['placeholder'] = '请输入%s'%(filed.label,)
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):
"""手机号校验的钩子"""
moile_phone = self.cleaned_data['mobile_phone']
# 判断短信模板是否有问题
tpl = self.request.GET.get('tpl')
tmplate_id = local_settings.TENCENT_SMS_TEMPLATE.get(tpl)
if not tmplate_id:
raise ValidationError("短信模板错误")
# 校验数据库中是否已经存在手机号
exists = models.UserInfo.objects.filter(mobile_phone=mobile_phone).exists()
if exists:
raise ValidationError("手机号已存在")
# 发送短信
code = random.randrange(1000,9999)
sms = send_sms_single(moile_phone, template_id, [code,])
if sms['result'] != 0:
raise ValidationError("短信发送失败{}".format(sms['errmsg']))
# 短信验证码写入redis(django-redis)
conn = get_redis_connection()
conn.set(moile_phone,code,ex=60)
return moile_phone
```
```python
# views目录下account.py文件
def send_sms(request):
"""发送短信"""
form = SendSmsForm(request,data=request.GET)
# 校验手机号:不能为空、格式是否正确
if form.is_valid():
return JsonResponse({'status':True})
return JsonResponse({'status':False,'error':form.errors})
```
#### 3.2.2.4 手机号校验
- 成功
- 倒计时
- 发送短信
- 将短信保存到redis中
- 失败
- 错误信息显示到前端页面
```javascript
/*点击获取验证码按钮绑定事件*/
function bindClickBtnSms() {
$('#btnSms').click(function(){
$('#btnSms').empty();
// 获取用户输入的手机号
// 根据手机号字段id值,获取手机号
var mobilePhone = $('#id_mobile_phone').val();
$.ajax({
"url":"{% url 'web:send_sms' %}",
"type":"GET",
"data":{"mobilePhone":mobilePhone,"tpl":"register"},
"dataType":"JSON", // 将服务器端返回的数据反序列化为字典
"success":function(res) {
if (res.status) {
sendSmsRemind();
} else {
// 输出错误信息
console.log(res); // {status: false, error: {mobile_phone:["错误信息"]}}
$.each(res.error,function(key,value){
$('#id_'+key).next().text(value[0]);
})
}
}
})
})
/*倒计时*/
function sendSmsRemind() {
var $btnSms = $('#btnSms');
$btnSms.prop('disabled',true);
var time = 60;
var remind = setInterval(function(){
$btnSms.val(time + "秒后重新发送");
time--;
if(time < 1) {
clearInterval(remind);
$btnSms.val('点击获取验证码').prop('disabled',false);
}
},1000)
}
}
```
#### 3.2.2.5 点击注册
- 点击收集数据&ajax请求
- 数据校验(每个字段)
- 写入数据库
```javascript
/*点击收集数据&ajax请求*/
function bindClickBtnSubmit() {
$('#btnSubmit').click(function() {
// 收集表单中数据
// $('#regForm').serialize(); // 所有字段的数据 + csrf token验证
// 通过ajax请求把数据发送给后台
$.ajax({
"url":"{% url 'web:register' %}",
"type":"POST",
"data":$('#regForm').serialize(), // 所有字段的数据 + csrf token验证
"dataType":"JSON",
"success":function(res) {
console.log(res);
}
})
})
}
```
- 数据校验
```python
# 数据校验在forms目录下account.py文件中进行校验
import random
from django import forms
from web import models
from django.core.validators import RegexValidator
from django.core.exceptions import ValidationError
from trance import local_settings
from utils.tencent.sms import send_sms_single
from django_redis import get_redis_connection
from utils import encrypt
class RegisterModelForm(forms.ModelForm):
mobile_phone = forms.CharField(label='手机号', validators=[RegexValidator(r'^(1[3|4|5|6|7|8|9])\d{9}$', '手机号格式错误'), ])
# password = forms.CharField(label='密码',
# widget=forms.PasswordInput(attrs={'class':"form-control",'placeholder':"请输入密码"}))
password = forms.CharField(label='密码',
min_length=8,
max_length=64,
error_messages = {
'min_length':"密码长度不能小于8个字符",
"max_length":"密码长度不能大于64个字符"
},
widget=forms.PasswordInput())
confirm_password = forms.CharField(label='确认密码',
min_length=8,
max_length=64,
error_messages= {
'min_length':"重复密码长度不能小于8个字符",
"max_length":"重复密码长度不能大于64个字符"
},
widget=forms.PasswordInput())
code = forms.CharField(label='验证码')
class Meta:
model = models.UserInfo
#fields = "__all__"
fields = ['username','password','confirm_password','email','mobile_phone','code']
def __init__(self,*args,**kwargs):
super().__init__(*args,**kwargs)
for name,filed in self.fields.items():
filed.widget.attrs['class'] = "form-control"
filed.widget.attrs['placeholder'] = '请输入%s'%(filed.label,)
def clean_username(self):
username = self.cleaned_data['username']
exists = models.UserInfo.objects.filter(username=username).exists()
if exists:
raise ValidationError('用户名已存在')
return username
def clean_password(self):
pwd = self.cleaned_data['password']
print("===============================")
print(pwd)
# 密码加密&返回
pwd = encrypt.md5(pwd)
print(pwd)
return pwd
def clean_confirm_password(self):
pwd = self.cleaned_data['password']
confirm_password = self.cleaned_data['confirm_password']
confirm_password = encrypt.md5(confirm_password)
print(confirm_password)
if pwd != confirm_password:
raise ValidationError('两次密码不一致')
return confirm_password
def clean_email(self):
email = self.cleaned_data["email"]
exists = models.UserInfo.objects.filter(email=email).exists()
if exists:
raise ValidationError('邮箱已存在')
return email
def clean_mobile_phone(self):
mobile_phone = self.cleaned_data['mobile_phone']
exists = models.UserInfo.objects.filter(mobile_phone=mobile_phone).exists()
if exists:
return ValidationError('手机号已注册')
return mobile_phone
def clean_code(self):
code = self.cleaned_data['code']
mobile_phone = self.cleaned_data['mobile_phone']
conn = get_redis_connection()
redis_code = conn.get(mobile_phone)
if not redis_code:
raise ValidationError("验证码失效或未发送,请重新获取验证码")
redis_str_code = redis_code.decode('utf8')
if code.strip() != redis_str_code:
raise ValidationError("验证码错误,请重新输入")
return code
```
- 密码长度校验的bug,产生原因

```python
# models.py文件中定义的模型类中密码属性max_lenhth值
from django.db import models
class UserInfo(models.Model):
username = models.CharField(verbose_name="用户名",max_length=32,db_index=True) # db_index 创建索引
email = models.EmailField(verbose_name="邮箱",max_length=32)
password = models.CharField(verbose_name="密码",max_length=64)
mobile_phone = models.CharField(verbose_name="手机号",max_length=15)
# forms文件夹account.py文件中密码属性max_length值不一致导致密码最大长度校验不通过
class RegisterModelForm(forms.ModelForm):
password = forms.CharField(label='密码',
min_length=8,
max_length=32,
error_messages = {
'min_length':"密码长度不能小于8个字符",
"max_length":"密码长度不能大于64个字符"
},
widget=forms.PasswordInput())
# 由于密码MD5加密后长度变为32位,所以模型中max_length的值应该设为比32位大或者不设置max_length
```
- 将已经注册成功信息再次输入一遍,页面不报错也不跳转

# 三、登录
## 3.1 登录页面展示
```html
{% extends 'layout/basic.html' %} {% load static %} {% block title %}用户登录{% endblock %} {% block css %}
{% endblock %} {% block content %}
{% endblock %}
{% block js %}
{% endblock %}
```
## 3.2 短信验证登录
```python
# bootstrap.py文件中内容
class BootStrapForm:
def __init__(self,*args,**kwargs):
super().__init__(*args,**kwargs)
for name,filed in self.fields.items():
filed.widget.attrs['class'] = "form-control"
filed.widget.attrs['placeholder'] = '请输入%s'%(filed.label,)
# 字段校验
import random
from django import forms
from web import models
from django.core.validators import RegexValidator
from django.core.exceptions import ValidationError
from trance import local_settings
from utils.tencent.sms import send_sms_single
from django_redis import get_redis_connection
from utils import encrypt
from web.forms.bootstrap import BootStrapForm
class LoginSms(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='验证码')
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:
return ValidationError('手机号不存在')
return mobile_phone
def clean_code(self):
code = self.cleaned_data['code']
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('utf8')
if code.strip() != redis_str_code:
raise ValidationError("验证码错误,请重新输入")
return code
```
## 3.3 用户名/密码&图片验证登录
参考链接: https://www.cnblogs.com/wupeiqi/articles/5812291.html
- 图片验证码思路

## 3.4 session&cookie

## 3.5 页面跳转
##
# 四、项目管理中心业务开发
## 4.1 django离线脚本
```python
django是一个框架
离线:非web运行时,也就是项目未启动状态
脚本:一个或几个py文件
django离线脚本:在某个或某几个py文件中对django离线项目做一些处理
```
示例:使用离线脚本往用户表中插入一条数据
```python
# scripts目录下的init_usr.py
import os
import sys
import django
# 将项目目录加入到环境变量
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
print('base_dir:',base_dir)
sys.path.append(base_dir)
# 加载配置文件,并启动一个虚拟的django服务
os.environ.setdefault("DJANGO_SETTINGS_MODULE","trance.settings")
django.setup()
from web import models
# 往数据库插入一条数据:连接数据库、操作、关闭连接
models.UserInfo.objects.create(username='tom',email='tom@qq.com',mobile_phone='18697247793',password='987654321')
models.UserInfo.objects.create(username='陈硕', email='chengshuo@live.com', mobile_phone='13838383838', password='123123')
```
django离线脚本其它业务应用场景:
- 往数据库中录入全国省市县(思路,网上下载一份省市县文件,读取文件数据直接写入数据库中)
- 朋友圈项目敏感字、词放入插入数据库用于敏感字词匹配
- saas平台免费版本:1G、5项目、10人
## 4.2 业务梳理&表结构设计
### 4.2.1 价格策略
| 分类 | 标题 | 价格/年 | 项目个数 | 项目成员/个 | 项目空间/个 | 单文件 | 创建时间 |
| ------ | ---------- | ------- | -------- | ----------- | ----------- | ------ | -------- |
| 免费版 | 个人免费版 | 0 | 5 | 5 | 10G | 2M | |
| 收费版 | VIP | 199 | 20 | 100 | 50G | 500M | |
| 收费版 | SVIP | 299 | 50 | 200 | 100G | 1G | |
| 其他 | | | | | | | |
注意:新用户注册拥有免费版额度。
### 4.2.2 用户和策略关联关系
**用户表**
| id | username | password | email | mobile_phone |
| ---- | -------- | --------- | ---------------- | ------------ |
| 1 | tom | 123456789 | tom@qq.com | 18716384539 |
| 2 | lucy | 987654321 | lucy@163.com | 11111111111 |
| | steven | asdasdasd | steven@aliyu.com | 22222222222 |
**交易记录表**
| id | 支付状态 | usr_id | price | 实际支付 | start_time | end_time | 数量(年) | 订单编号 |
| ---- | -------- | ------ | ----- | -------- | ---------- | -------- | ---------- | -------- |
| 1 | 已支付 | 1 | 1 | 0 | 2020/7/7 | null | 0 | UY12 |
| 2 | 已支付 | 2 | 1 | 0 | 2020/7/7 | null | 0 | UY12 |
| 3 | 已支付 | 3 | 1 | 0 | 2020/7/7 | null | 0 | UY12 |
| 4 | 已支付 | 2 | 2 | 199 | 2020/8/7 | 2021/8/7 | 1 | UY12 |
| 5 | 已支付 | 3 | 3 | 299*2 | 2020/5/4 | 2022/5/4 | 2 | UY12 |
`request.trance = 交易对象`
### 4.2.3 创建存储
基于腾讯对象存储COS存储数据。
### 4.2.4 项目表
| ID | 项目名称 | 描述 | 颜色 | 是否星标 | 参与人数 | 创建者 | 已使用空间 |
| ---- | -------- | ---- | ----- | -------- | -------- | ------ | ---------- |
| 1 | CRM | ... | green | false | 5 | 3 | 5M |
| 2 | PASS | ... | red | true | 10 | 3 | 1G |
| 3 | SAAS | ... | pink | true | 20 | 3 | 2G |
### 4.25 项目参与者
| ID | 项目ID | 用户ID | 星标 | 邀请者(选作) | 角色权重(选作) |
| ---- | ------ | ------ | ----- | -------------- | ---------------- |
| 1 | 1 | 1 | true | 3 | 1 |
| 2 | 1 | 2 | false | 3 | 2 |
| | | | | | |
| | | | | | |
| | | | | | |
## 4.3 功能实现
- 查看项目列表
- 创建项目
```html
```
- 通过django中ModeForm类实现模态对话框
```python
from django import forms
from web import models
from web.forms.bootstrap import BootStrapForm
class ProjectModelForm(BootStrapForm,forms.ModelForm):
# desc = forms.CharField(widget=forms.Textarea())
class Meta:
model = models.Project
fields = ['name','color','desc']
widget = {
'desc':forms.Textarea
}
```
```html
{% csrf_token %}
{% for field in form %}
{{ form.label }}
{{ field }}
{% endfor %}
```
- 星标项目

- 展示项目
1.数据处理

2.样式展示
```html
>
```
- 星标项目
```python
我创建的项目:Project的表star = True
我参与的项目:ProjectUser的表star = True
```
- 移除星标
```python
我创建的项目:Project的表star = False
我参与的项目:ProjectUser的表star = False
```
- 添加项目:颜色选择(难点)
- 部分样式应用BootStrap
```python
class BootStrapForm:
# 自定义类属性
bootstrap_class_exclude = []
def __init__(self,*args,**kwargs):
super().__init__(*args,**kwargs)
for name,filed in self.fields.items():
if name in self.bootstrap_class_exclude:
continue
filed.widget.attrs['class'] = "form-control"
filed.widget.attrs['placeholder'] = '请输入%s'%(filed.label,)
#class Foo(BootStrapForm):
# bootstrap_class_exclude = ['color']
#obj = Foo()
```
```python
class ProjectModelForm(BootStrapForm,forms.ModelForm):
bootstrap_class_exclude = ['color']
# desc = forms.CharField(widget=forms.Textarea())
class Meta:
model = models.Project
fields = ['name','color','desc']
widgets = {
'desc':forms.Textarea,
'color':forms.ColorRadioSelect(attrs={
'class':'color-radio'
})
#'color':forms.RadioSelect
```
- 定制ModelForm插件
```python
from web.forms.widgets import ColorRadioSelect
class ProjectModelForm(BootStrapForm,forms.ModelForm):
bootstrap_class_exclude = ['color']
# desc = forms.CharField(widget=forms.Textarea())
class Meta:
model = models.Project
fields = ['name','color','desc']
widgets = {
'desc':forms.Textarea,
'color':forms.ColorRadioSelect(attrs={
'class':'color-radio'
})
#'color':forms.RadioSelect
```
```python
from django.forms import RadioSelect
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'
```
```html
{% with id=widget.attrs.id %}
{% if id %} id="{{ id }}"{% endif %}{% if widget.attrs.class %} class="{{ widget.attrs.class }}"{% endif %}>
{% for group, options, index in widget.optgroups %}
{% for option in options %}
{% include option.template_name with widget=option %}
{% endfor %}
{% endfor %}
{% endwith %}
```
```html
{% include "django/forms/widgets/input.html" %}
```
- 项目切换
```
1.数据库中获取
我创建的
我参与的
2.循环显示
3.当前页面需要显示/其他页面也需要显示 [inclusion_tag]
```
- 项目管理菜单处置
- 路由规划
```
/manage/项目ID/dashhoard/
/manage/项目ID/issues/
/manage/项目ID/statistics/
/manage/项目ID/file/
/manage/项目ID/wiki/
/manage/项目ID/setting/
```
- 判断是否已进入项目【中间件中实现】
URL是否以manage开头 & project_id是我创建的 or 我参与的
- 进入项目展示菜单
```html
```
# 五、wiki功能实现
## 5.1 wiki表结构设计
| ID | 标题 | 内容 | 项目ID | 父ID | depth(深度) |
| ---- | -------------- | -------------------- | ------ | ---- | ----------- |
| 1 | 篮球之神 | 乔老爷子公牛王朝 | 1 | null | 1 |
| 2 | 最接近神的男人 | 科比总决赛之旅 | 1 | null | 1 |
| 3 | 择一城中路 | 洛维斯基与小牛情深深 | 1 | 1 | 2 |
```python
class Wiki(models.Model):
title = models.CharField(verbose_name='标题',max_length=32)
project = models.ForeignKey(verbose_name='项目', to='Project',on_delete=models.CASCADE)
content = models.TextField(verbose_name='文章内容')
# 自关联,to='self'
parent = models.ForeignKey(verbose_name='父文章',to='self',on_delete=models.CASCADE,null=True,blank=True,related_name='children')
```
## 5.2 wiki开发流程
## 5.3 wiki首页
## 5.4 wiki添加
## 5.5 wiki添加bug修复
BUG现象:在不同项目下都展示了相应的父级文章


解决方案:
```python
from web.forms.bootstrap import BootStrapForm
from django import forms
from web import models
class WikiModelForm(BootStrapForm,forms.ModelForm):
class Meta:
model = models.Wiki
exclude = ['project']
def __init__(self,request,*args,**kwargs):
super().__init__(*args,**kwargs)
self.request = request
# 找到想要的字段把它绑定显示数据重置
# 数据 = 去数据库中获取 当前项目所有的wiki标题
total_data_list = [("","---请选择---"),]
data_list = models.Wiki.objects.filter(project=self.request.saas.project).values_list('id','title')
total_data_list.extend(data_list)
#self.fields['parent'].choices = [(1,'xxx',2'xxx)]
self.fields['parent'].choices = total_data_list
```
## 5.6 wiki多级目录展示实现思路
```
思路一:
模板渲染
- 数据库中获取数据要有层级的划分
querset = model.Wiki.object.filter(project_id=2)
构造数据结构:
data = [
{
id:1,
title:'xxx',
children:[
{
id:'xxxx',
title:'xxx',
...
}
]
}
]
- 页面显示,循环显示data(不知道有多少层)
递归
缺点:
1.数据构造比较复杂
2.递归实现难度较大,效率低
```
```
思路二:后端 + 前端(ID选择器 + ajax请求)
- 前端:打开页面之后,发送ajax请求获取所有文档标题信息
- 后端:获取所有文章信息
data = model.Wiki.object.filter(project_id=2).values_list('id','title','parent_id')
data返回的数据结构如下:
[
{'id',1,'title','xxx','parent_id':None},
{'id',2,'title','xxx','parent_id':None},
{'id',3,'title','xxx','parent_id':None},
{'id',4,'title','xxx','parent_id':3},
{'id',5,'title','xxx','parent_id':2},
{'id',6,'title','xxx','parent_id':1},
]
将data数据直接返回给前端
- ajax的回调函数success中获取res.data,并循环:
$.each(res.data,function(index,item){
if (item.parent_id) {
} else {
}
})
# 前端页面展示结构
```
多级目录展示存在两个问题
- (子目录更改父目录)父目录要提前出现:排序 + 字段(深度depth)能解决该问题
- 不具备文章预览功能
## 5.7 预览文章
## 5.8 wiki删除
## 5.9 wiki编辑
# 六 、markdown编辑器
- 富文本编辑器,ckeditor
- makrdown编辑器,mdeditor
项目中应用makrdown编辑器:
- 添加和编辑页面中textarea输入框——>转换为markdown编辑器
```
- 应用markdown的css样式
- 应用JS
https://www.mdeditor.com/
htps://github.com/pandao/editor.md
https://pandap.github.io/editor.md/examples/index.html
```
```
1.textarea框通过div包裹以便以后查找并转换为编辑器
{{ filed }}
2.应用makrdownjs和css样式
3.进行初始化
$(function() {
initEditorMd();
});
/*
初始化markdown编辑器(textare转换为编辑器)
*/
function initEditorMd() {
editormd('editor', {
placeholder: "请输入内容",
height: 500,
path: "{% static 'plugin/editor-md/lib/' %}",
imageUpload:true,
imageFormats:["jpg",'jpeg','png','gif'],
imageUploadURL:WIKI_UPLOAD_URL
})
}
```
## 6.1 添加
## 6.2 编辑
## 6.3 预览页面
## 6.4 基于python创建桶
```python
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
secret_id = 'AKIDFPJSXQEk8PXVL3Tx5zf6MSL0Sf7Qoikg' # 替换为用户的 secretId
secret_key = 'yiCWfZCXcQxJZlqncKvRu5DKHySg8sMp' # 替换为用户的 secretKey
region = 'ap-chengdu' # 替换为用户的 Region
config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key)
client = CosS3Client(config)
response = client.create_bucket(
Bucket='test-1251317460',
ACL="public-read" # private / public-read / public-read-write
)
```
## 6.5 腾讯对象存储

### 6.5.1 开通腾讯云的对象存储服务器


### 6.5.2 python实现cos快速上传文件

#### 6.5.2.1 安装SDk
```python
pip install -U cos-python-sdk-v5
```
#### 6.5.2.2 开始使用
- 初始化
```python
# -*- coding=utf-8
# appid 已在配置中移除,请在参数 Bucket 中带上 appid。Bucket 由 BucketName-APPID 组成
# 1. 设置用户配置, 包括 secretId,secretKey 以及 Region
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
import sys
import logging
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
secret_id = 'COS_SECRETID' # 替换为用户的 secretId
secret_key = 'COS_SECRETKEY' # 替换为用户的 secretKey
region = 'COS_REGION' # 替换为用户的 Region
token = None # 使用临时密钥需要传入 Token,默认为空,可不填
scheme = 'https' # 指定使用 http/https 协议来访问 COS,默认为 https,可不填
config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key, Token=token, Scheme=scheme)
# 2. 获取客户端对象
client = CosS3Client(config)
# 参照下文的描述。或者参照 Demo 程序,详见 https://github.com/tencentyun/cos-python-sdk-v5/blob/master/qcloud_cos/demo.py
```
- 创建存储桶
```python
response = client.create_bucket(
Bucket='examplebucket-1250000000'
)
```
- 查询村村桶
```python
response = client.list_buckets(
)
```
- 上传对象( 简单上传不支持超过5G的文件,推荐使用下方高级上传接口。 )
```python
#### 文件流简单上传(不支持超过5G的文件,推荐使用下方高级上传接口)
# 强烈建议您以二进制模式(binary mode)打开文件,否则可能会导致错误
with open('picture.jpg', 'rb') as fp:
response = client.put_object(
Bucket='examplebucket-1250000000',
Body=fp,
Key='picture.jpg',
StorageClass='STANDARD',
EnableMD5=False
)
print(response['ETag'])
#### 字节流简单上传
response = client.put_object(
Bucket='examplebucket-1250000000',
Body=b'bytes',
Key='picture.jpg',
EnableMD5=False
)
print(response['ETag'])
#### chunk 简单上传
import requests
stream = requests.get('https://cloud.tencent.com/document/product/436/7778')
# 网络流将以 Transfer-Encoding:chunked 的方式传输到 COS
response = client.put_object(
Bucket='examplebucket-1250000000',
Body=stream,
Key='picture.jpg'
)
print(response['ETag'])
#### 高级上传接口(推荐)
# 根据文件大小自动选择简单上传或分块上传,分块上传具备断点续传功能。
response = client.upload_file(
Bucket='examplebucket-1250000000',
LocalFilePath='local.txt', # 本地文件路径
Key='picture.jpg', # 上传到桶之后的文件名
PartSize=1,
MAXThread=10,
EnableMD5=False
)
print(response['ETag'])
```

## 6.6 项目中集成对象存储
希望项目中上传的图片上传至腾讯云的COS对象中,防止我们服务器处理图片时压力过大。
### 6.6.1 创建项目时创建项目对应的桶
### 6.6.2 markdown上传图片到COS
# 七、文件管理
功能介绍:
- 文件夹管理
- 文件管理
知识点:
- 模态对话框& ajax & 后台ModelForm校验
- 目录切换:展示当前文件夹&文件
- 删除文件夹:嵌套的子文件&子文件夹 全部删除
- js上传文件到cos(wiki用python上传文件到cos)
- 进度条操作
- 删除文件
- 数据库中删除
- cos中文件也需要删除
- 下载文件
## 7.1 功能实现思路
## 7.2 表结构设计
| ID | 项目ID | 文件/文件夹名 | 类型 | 大小 | 父目录 | key | 更新者 | 更新时间 |
| ---- | ------ | ------------- | ---- | ---- | ------ | ---------------- | ------ | -------- |
| 1 | 9 | a | 2 | null | null | null | | |
| 2 | 9 | b | 2 | null | null | null | | |
| 3 | 9 | 12.png | 1 | 1000 | null | adsfadfa.png | | |
| 4 | 9 | 12.png | 1 | 1000 | null | ewqrqewrqwe.png | | |
| 5 | 9 | aa | 2 | null | 1 | null | | |
| 6 | 9 | aa.txt | 2 | 90 | 1 | qrewrqwerqwe.txt | | |
| 7 | 9 | c | 2 | null | 5 | null | | |
```python
class FileRepository(models.Model):
""" 文件库 """
project = models.ForeignKey(verbose_name='项目', to='Project')
file_type_choices = (
(1, '文件'),
(2, '文件夹')
)
file_type = models.SmallIntegerField(verbose_name='类型', choices=file_type_choices)
name = models.CharField(verbose_name='文件夹名称', max_length=32, help_text="文件/文件夹名")
key = models.CharField(verbose_name='文件储存在COS中的KEY', max_length=128, null=True, blank=True)
# int类型最大表示的数据
file_size = models.BigIntegerField(verbose_name='文件大小', null=True, blank=True, help_text='字节')
file_path = models.CharField(verbose_name='文件路径', max_length=255, null=True,
blank=True) # https://桶.cos.ap-chengdu/....
parent = models.ForeignKey(verbose_name='父级目录', to='self', related_name='child', null=True, blank=True)
update_user = models.ForeignKey(verbose_name='最近更新者', to='UserInfo')
update_datetime = models.DateTimeField(verbose_name='更新时间', auto_now=True)
```
## 7.3 知识点
### 7.3.1 URL传参
```python
# file
# manage/3/file/?folder_id=50
url(r'^manage/(?P\d+)/file/$',projectManage.file, name='file')
def file(request,project_id):
if request.method == 'GET':
folder_id = request.GET.get('folder_id')
# 根据folder_id判断用户有无传参
```
### 7.3.2 模态框&警告框结合
bootstrap框架javascript插件中找模态框+警告框组合
### 7.3.3 导航条
| ID | 项目ID | 文件/文件夹名 | 类型 | 大小 | 父目录 | key | 更新者 | 更新时间 |
| ---- | ------ | ------------- | ---- | ---- | ------ | ---------------- | ------ | -------- |
| 1 | 9 | a | 2 | null | null | null | | |
| 2 | 9 | b | 2 | null | null | null | | |
| 3 | 9 | 12.png | 1 | 1000 | null | adsfadfa.png | | |
| 4 | 9 | 12.png | 1 | 1000 | null | ewqrqewrqwe.png | | |
| 5 | 9 | aa | 2 | null | 1 | null | | |
| 6 | 9 | aa.txt | 2 | 90 | 1 | qrewrqwerqwe.txt | | |
| 7 | 9 | c | 2 | null | 5 | null | | |
```python
# file
# manage/3/file/?folder_id=50
url(r'^manage/(?P\d+)/file/$',projectManage.file, name='file')
def file(request,project_id):
url_list = [
{id:1,'name':'a','parent_id':null},
{id:5,'name':'aa','parent_id':1},
{id:7,'name':'c','parent_id':5}
]
if request.method == 'GET':
folder_id = request.GET.get('folder_id')
# 根据folder_id判断用户有无传参
if not folder_id:
pass
else:
file_object = models.FileRepository.objects.filter(id=folder_id,file_type=2).first()
row_object = file_object
while row_object:
url_list.insert(0,{'id':row_object.id,'name':row_object.name,'parent_id':row_object.parent_id})
print(url_list)
```
### 7.3.4 COS上传文件
#### 7.3.4.1 python实现
```python
def upload_file(bucket, region, file_object, key):
config = CosConfig(Region=region, SecretId=local_settings.TENCENT_COS_ID, SecretKey=local_settings.TENCENT_COS_KEY)
client = CosS3Client(config)
response = client.upload_file_from_buffer(
Bucket=bucket,
Body=file_object, # 文件对象
Key=key # 上传到桶之后的文件名
)
# https://wangyang-1251317460.cos.ap-chengdu.myqcloud.com/p1.png
return "https://{}.cos.{}.myqcloud.com/{}".format(bucket, region, key)
```
注意:
1. python上传cos密钥时安全的
2. 还可以通过python查看桶信息,删除文件、下载文件(可参考腾讯云API文档/SDK文档)
#### 7.3.4.2 js直接上传实现(建议参考官方SDK指导)

在前端可以通过xtengJS+密钥直接向腾讯的COS上传文件,但是考虑安全问题所以不推荐使用。
第一步:下载js并引入
下载地址: https://github.com/tencentyun/cos-js-sdk-v5/tree/master/dist
```html
```
第二步:前端代码示例
```html
{% load static %}
Document
直接通过密钥上传文件
```
第三步:跨域问题(浏览器导致)


解决跨域问题:

第四步:上传成功
#### 7.3.4.3 临时密钥上传COS(推荐)

- 路由
```python
url(r'^/demo/$',manage.demo, name='demo'),
url(r'^/cos/credential/$',manage.cos_credential, name='cos_credential')
```
- 视图
```python
pip install -U qcloud-python-sts
import json
import os
from sts.sts import Sts
def demo(request):
return render(request,'demo.html')
# 获取临时凭证视图
def cos_credential(request):
# 生成一个临时凭证并返回给前端
# 1.安装一个生成一个临时凭证的python模块 pip install -U qcloud-python-sts
config = {
# 临时密钥有效时长,单位是秒
'duration_seconds': 1800,
'secret_id': os.environ['COS_SECRET_ID'],
# 固定密钥
'secret_key': os.environ['COS_SECRET_KEY'],
# 设置网络代理
# 'proxy': {
# 'http': 'xx',
# 'https': 'xx'
# },
# 换成你的 bucket
'bucket': 'example-1253653367',
# 换成 bucket 所在地区
'region': 'ap-guangzhou',
# 这里改成允许的路径前缀,可以根据自己网站的用户登录态判断允许上传的具体路径
# 例子: a.jpg 或者 a/* 或者 * (使用通配符*存在重大安全风险, 请谨慎评估使用)
'allow_prefix': 'exampleobject',
# 密钥的权限列表。简单上传和分片需要以下的权限,其他权限列表请看 https://cloud.tencent.com/document/product/436/31923
'allow_actions': [
# 简单上传
'name/cos:PutObject',
'name/cos:PostObject',
# 分片上传
'name/cos:InitiateMultipartUpload',
'name/cos:ListMultipartUploads',
'name/cos:ListParts',
'name/cos:UploadPart',
'name/cos:CompleteMultipartUpload'
],
}
try:
sts = Sts(config)
response = sts.get_credential()
print('get data : ' + json.dumps(dict(response), indent=4))
except Exception as e:
print(e)
```
- 前端页面
```html
{% load static %}
Document
直接通过密钥上传文件
```
#### 7.3.4.4 如何将cos的功能集成到项目中
```python
# wiki上传功能已经实现了创建桶的功能,但并未解决浏览器跨域问题
def project_list(request):
...
# POST,对话框ajax添加项目
form = ProjectModelForm(request,data=request.POST)
if form.is_valid():
name = form.cleaned_data['name']
#1. 为项目创建一个桶(桶命名规则不能出现汉字,可利用python汉字转拼音模块)& 桶添加跨域规则
bucket = "{}-{}-{}-1302101842".format(name,request.saas.user.mobile_phone, str(int(time.time())))
region = 'ap-chengdu'
create_bucket(bucket,region)
# 表单验证通过:项目名、颜色、描述(默认只有这三项)+ creator谁创建的项目
# 创建者等于登录用户(request.saas.user 中间件中已经定义过了)
form.instance.creator = request.saas.user
form.instance.bucket = bucket
form.instance.region = region
#2. 创建项目(确保当前用户有无权限新建项目,以及是否还有额度)
form.save()
return JsonResponse({'status':True})
# 表单验证未通过显示错误信息
return JsonResponse({'status':False,'error':form.errors})
```
```python
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
from trance import local_settings
def create_bucket(bucket,region = 'ap-chengdu'):
'''
创建桶
param: bucket 桶名称
param: regin 区域
'''
secret_id = local_settings.TENCENT_COS_ID # 替换为用户的 secretId
secret_key = local_settings.TENCENT_COS_KEY # 替换为用户的 secretKey
config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key)
client = CosS3Client(config)
response = client.create_bucket(
Bucket= bucket,
ACL="public-read" # private / public-read / public-read-write
)
# 解决跨域问题
cors_config = {
'CORSRule': [
{
'AllowedOrigin': '*',
'AllowedMethod': ['GET', 'PUT', 'HEAD', 'POST', 'DELETE'],
'AllowedHeader': "*",
'ExposeHeader': "*",
'MaxAgeSeconds': 500
}
]
}
client.put_bucket_cors(
Bucket=bucket,
CORSConfiguration=cors_config
)
```
**注意:cos集成到项目需要的改动**
1. python临时凭证的获取: 当前项目的桶&区域p(request.saas.projec.bucket或...)
2. js上传文件: 获取当前项目的桶&区域
3. 获取临时凭证&上传文件:创建项目跨域加上
4. 上传文件临时凭证书写位置:
- 全局:默认超时之后,自动再次获取(官方推荐)
- 局部:每次上传文件之前,进行临时凭证的获取
### 7.3.5 this
```javascript
var name = '全栈28期';
function func() {
var name = '全栈25期';
console.log(name);
}
func(); // 全栈25期
```
```javascript
var name = '全栈28期';
function func() {
var name = '全栈25期';
console.log(this.name);
}
// window.func(); this就是window
func(); // 全栈28期
```
```javascript
var name = '刘德华';
info = {
name:'张学友',
func:function(){
console.log(this.name);
}
}
info.func(); // 张学友
```
```javascript
var name = '刘德华';
info = {
name:'张学友',
func:function(){
console.log(this.name) // 张学友
function test() {
console.log(this.name); // 刘德华
}
test(); // window.test()
}
}
info.func();
```
```javascript
var name = '刘德华';
info = {
name:'张学友',
func:function(){
var that = this;
function test() {
console.log(that.name); // 当前函数中找that变量,没有,然后去父级找,父级里面这个that是属于info的,所以此时that赋值的this就指向了info,that.name就等于info.name
}
test(); // window.test()
}
}
info.func();
```
总结:每个函数都有一个作用域,在它的内部都会存在this,谁调用的函数,那么this就是谁。
### 7.3.6 闭包和异步
## 7.4 文件夹管理
### 7.4.1 创建文件夹
### 7.4.2 文件夹列表展示
### 7.4.3 文件夹路径导航
### 7.4.4 文件夹编辑
#### 7.4.5 文件夹删除(DB级联删除&删除COS文件)
## 7.5 文件管理
### 7.5.1 上传按钮
### 7.5.2 获取临时凭证&上传文件
```python
{% block js %}
{% endblock %}
```
```python
# utils/tencent/cos.py
import json
import os
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
from trance import local_settings
from sts.sts import Sts
# 获取临时凭证视图
def credential(bucket, region):
""" 获取cos上传临时凭证 """
config = {
# 临时密钥有效时长,单位是秒(30分钟=1800秒)
'duration_seconds': 1800,
# 固定密钥 id
'secret_id': local_settings.TENCENT_COS_ID,
# 固定密钥 key
'secret_key': local_settings.TENCENT_COS_KEY,
# 换成你的 bucket
'bucket': bucket,
# 换成 bucket 所在地区
'region': region,
# 这里改成允许的路径前缀,可以根据自己网站的用户登录态判断允许上传的具体路径
# 例子: a.jpg 或者 a/* 或者 * (使用通配符*存在重大安全风险, 请谨慎评估使用)
'allow_prefix': '*',
# 密钥的权限列表。简单上传和分片需要以下的权限,其他权限列表请看 https://cloud.tencent.com/document/product/436/31923
'allow_actions': [
# "name/cos:PutObject",
# 'name/cos:PostObject',
# 'name/cos:DeleteObject',
# "name/cos:UploadPart",
# "name/cos:UploadPartCopy",
# "name/cos:CompleteMultipartUpload",
# "name/cos:AbortMultipartUpload",
"*",
],
}
sts = Sts(config)
result_dict = sts.get_credential()
return result_dict
```
```python
# views/file.py
from django.http import HttpResponse,JsonResponse
from django.shortcuts import render,redirect
from utils.tencent.cos import credential
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def cos_credential(request,project_id):
"""获取cos授权凭证"""
data_dict =credential(request.saas.project.bucket,request.saas.project.region)
return JsonResponse(data_dict)
```
### 7.5.3 上传文件大小限制
扩展:ajax向后台发送消息
```python
前端:
$.ajax({
data:{name:'xxx',age:15,xx:[11,22,33]}
})
Django后端:
request.POST
request.POST.get('name')
request.POST.get('age')
reqquest.POST.get('xx')
```
```python
前端:
$.ajax({
data:JSON.stringify({name:{k1:1,k2:3},age:15,xx:[11,22,[33,44,55,66]})
})
Django后端:
info = json.loads(request.body.decode('utf-8'))
info['name']
info['xxx']
#注意:
$.post(url,data,callback)
```
```javascript
// 如果将临时凭证获取作为全局变量,则无法获取上传文件数量以及上传文件大小
function bindUploadFile() {
$('#uploadFile').change(function() {
$('#progressList').empty();
var fileList = $(this)[0].files;
// 获取本次要上传的每个文件 名称&大小
var checkFileList = [];
$.each(fileList, function(index, fileObject) {
checkFileList.push({
'name': fileObject.name,
'size': fileObject.size
})
});
// 把这些数据发送到django后台:Django后台进行容量的校验,如果么有问题则返回临时凭证;否则返回错误信息;
var cos_credential = new COS({
getAuthorization: function (options, callback) {
// 向django后台发送请求(并携带参数传递给后台),获取临时凭证
// django后台检验完数据后,将return JsonResponse({'status':True,'data':data_dict}),前端通过res接受返回参数
$.post(COS_CREDENTIAL,JSON.stringify(checkFileList),function (res) {
//console.log(res.data);
if (res.status) {
var credentials = res.data && res.data.credentials;
if (!res.data || !res.data.credentials) return console.error('credentials invalid');
callback({
TmpSecretId: credentials.tmpSecretId,
TmpSecretKey: credentials.tmpSecretKey,
XCosSecurityToken: credentials.sessionToken,
StartTime: res.data.startTime, // 时间戳,单位秒,如:1580000000,建议返回服务器时间作为签名的开始时间,避免用户浏览器本地时间偏差过大导致签名错误
ExpiredTime: res.data.expiredTime, // 时间戳,单位秒,如:1580000900
});
}else {
alert(res.error);
}
})
}
})
// 上传文件(上传之前先获取临时凭证
$.each(fileList, function(index, fileObject) {
var fileName = fileObject.name;
var key = (new Date()).getTime() + "_" + fileName;
// 上传文件(异步)
cos_credential.putObject({
Bucket: '{{ request.saas.project.bucket }}',
Region: '{{ request.saas.project.region }}',
Key: key,
Body: fileObject, // 上传文件对象
onProgress: function(progressData) {
console.log("文件上传进度---------->",fileName,JSON.stringify(progressData));
}
}, function(err, data) {
console.log(err || data)
});
})
});
}
```
```python
@csrf_exempt
def cos_credential(request,project_id):
"""获取cos授权凭证"""
# 单文件限制大小(M):request.saas.price_policy.per_file_size
per_file_limit = request.saas.price_policy.per_file_size * 1024 * 1024
file_list = json.loads(request.body.decode('utf-8'))
total_file_limit = request.saas.price_policy.project_space * 1024 * 1024 * 1024
total_size = 0
for item in file_list:
# 上传的文件字节(B)大小:item['size]
if item['size'] > per_file_limit:
msg = "单文件超出(最大限制{}M),文件{},请升级套餐!!".format(request.saas.price_policy.per_file_size,item['name'])
return JsonResponse({'status':False,'error':msg})
total_size += item[size]
# 总容量进行限制
# request.saas.price_policy.project_space # 项目允许的空间
# request.saas.project.use_space # 项目已使用的空间
if total_size + request.saas.project.use_space > total_file_limit:
return JsonResponse({'status':False,'error':"容量超出限制,请升级套餐!!"})
data_dict =credential(request.saas.project.bucket,request.saas.project.region)
return JsonResponse({'status':True,'data':data_dict})
```
前端JSON传参错误:



正确传参:

### 7.5.4 上传文件展示进度条
```html
```
```javascript
function bindUploadFile() {
$('#uploadFile').change(function() {
// 每次上传完毕之后清空之前上传记录,重新进行下一次上传
//$('#progressList').empty();
var fileList = $(this)[0].files;
// 获取本次要上传的每个文件 名称&大小
var checkFileList = [];
$.each(fileList, function(index, fileObject) {
checkFileList.push({
'name': fileObject.name,
'size': fileObject.size
})
});
// 把这些数据发送到django后台:Django后台进行容量的校验,如果么有问题则返回临时凭证;否则返回错误信息;
var cos_credential = new COS({
getAuthorization: function (options, callback) {
// 向django后台发送请求,获取临时凭证
$.post(COS_CREDENTIAL,JSON.stringify(checkFileList),function (res) {
console.log('111111111111111111111');
console.log(res);
//console.log(res.data);
if (res.status) {
var credentials = res.data && res.data.credentials;
if (!res.data || !res.data.credentials) return console.error('credentials invalid');
callback({
TmpSecretId: credentials.tmpSecretId,
TmpSecretKey: credentials.tmpSecretKey,
XCosSecurityToken: credentials.sessionToken,
StartTime: res.data.startTime, // 时间戳,单位秒,如:1580000000,建议返回服务器时间作为签名的开始时间,避免用户浏览器本地时间偏差过大导致签名错误
ExpiredTime: res.data.expiredTime, // 时间戳,单位秒,如:1580000900
});
// 授权成功才在页面上显示进度条
$('#uploadProgress').removeClass('hide');
}else {
alert(res.error);
}
})
}
})
// 上传文件(上传之前先获取临时凭证
$.each(fileList, function(index, fileObject) {
var fileName = fileObject.name;
//var fileSize = fileObject.size;
var key = (new Date()).getTime() + "_" + fileName;
var tr = $('#progressTemplate').find('tr').clone();
tr.find('.name').text(fileName);
$('#progressList').append(tr);
// 上传文件(异步)
cos_credential.putObject({
Bucket: '{{ request.saas.project.bucket }}',
Region: '{{ request.saas.project.region }}',
Key: key,
Body: fileObject, // 上传文件对象
onProgress: function(progressData) {
console.log("文件上传进度---------->",fileName,JSON.stringify(progressData));
var percent = progressData.percent * 100 + '%';
tr.find('.progress-bar').text(percent);
tr.find('.progress-bar').css('width', percent);
}
}, function(err, data) {
console.log(err || data)
});
})
});
}
```
### 7.5.5 上传成功向后台发送数据
```javascript
function bindUploadFile() {
$('#uploadFile').change(function() {
// 每次上传完毕之后清空之前上传记录,重新进行下一次上传
$('#progressList').empty();
var fileList = $(this)[0].files;
// 获取本次要上传的每个文件 名称&大小
var checkFileList = [];
$.each(fileList, function(index, fileObject) {
checkFileList.push({
'name': fileObject.name,
'size': fileObject.size
})
});
// 把这些数据发送到django后台:Django后台进行容量的校验,如果么有问题则返回临时凭证;否则返回错误信息;
var cos_credential = new COS({
getAuthorization: function (options, callback) {
// 向django后台发送请求,获取临时凭证
$.post(COS_CREDENTIAL,JSON.stringify(checkFileList),function (res) {
//console.log(res.data);
if (res.status) {
var credentials = res.data && res.data.credentials;
if (!res.data || !res.data.credentials) return console.error('credentials invalid');
callback({
TmpSecretId: credentials.tmpSecretId,
TmpSecretKey: credentials.tmpSecretKey,
XCosSecurityToken: credentials.sessionToken,
StartTime: res.data.startTime, // 时间戳,单位秒,如:1580000000,建议返回服务器时间作为签名的开始时间,避免用户浏览器本地时间偏差过大导致签名错误
ExpiredTime: res.data.expiredTime, // 时间戳,单位秒,如:1580000900
});
// 授权成功才在页面上显示进度条
$('#uploadProgress').removeClass('hide');
}else {
alert(res.error);
}
})
}
})
// 上传文件(上传之前先获取临时凭证
$.each(fileList, function(index, fileObject) {
var fileName = fileObject.name;
var fileSize = fileObject.size;
var key = (new Date()).getTime() + "_" + fileName;
var tr = $('#progressTemplate').find('tr').clone();
tr.find('.name').text(fileName);
$('#progressList').append(tr);
// 上传文件(异步)
cos_credential.putObject({
Bucket: '{{ request.saas.project.bucket }}',
Region: '{{ request.saas.project.region }}',
Key: key,
Body: fileObject, // 上传文件对象
onProgress: function(progressData) {
console.log("文件上传进度---------->",fileName,JSON.stringify(progressData));
var percent = progressData.percent * 100 + '%';
tr.find('.progress-bar').text(percent);
tr.find('.progress-bar').css('width', percent);
}
}, function(err, data) {
console.log(err || data)
if (data && data.statusCode == 200){
// 上传成功,将本次上传文件信息发送后台校验并写入数据库
// 当前文件上传成功
$.post(FILE_POST,{
name:fileName,
file_size:fileSize,
key:key,
parent: CURRENT_FOLDER_ID,
etag:data.ETag,
file_path: data.Location
},function (res) {
console.log(res);
})
} else {
tr.find('.progress-error').text('上传失败');
}
});
})
});
}
```
```python
@csrf_exempt
def file_post(request,project_id):
"""
已上传成功的文件属性校验&写入数据库
name:fileName,
file_size:fileSize,
key:key,
parent: CURRENT_FOLDER_ID,
etag:data.ETag,
file_path: data.Location
"""
print(request.POST)
# 根据key再去cos获取文件Etag和和前端上传成功后的Etag"db7c0d83e50474f934fd4ddf059406e5"
# 把获取的数据写入数据库
return JsonResponse({})
```


### 7.5.6 后台对文件数据校验
```python
# cos.py文件
import json
import os
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
from trance import local_settings
from sts.sts import Sts
def check_file(bucket, region, key):
config = CosConfig(Region=region, SecretId=local_settings.TENCENT_COS_ID, SecretKey=local_settings.TENCENT_COS_KEY)
client = CosS3Client(config)
data = client.head_object(
Bucket=bucket,
Key=key
)
return data
```
```python
class FileModelForm(forms.ModelForm):
etag = forms.CharField(label='Etag')
class Meta:
model = models.FileRepository
exclude = ['project','file_type','update_user','update_datetime']
def __init__(self,request,*args,**kwargs):
super().__init__(*args,**kwargs)
self.request = request
def clean_file_path(self):
return "https://{}".format(self.cleaned_data['file_path'])
def clean(self):
key = self.cleaned_data['key']
etag = self.cleaned_data['etag']
size = self.cleaned_data['file_size']
if not key or not etag:
return self.cleaned_data
# 向COS校验是否合法
# sdk功能
from qcloud_cos.cos_exception import CosServiceError
try:
result = check_file(self.request.saas.project.bucket, self.request.saas.project.region, key)
print('result-------------------->',result)
except CosServiceError as e:
self.add_error("key", '文件不存在')
return self.cleaned_data
cos_etag = result.get('ETag')
if etag != cos_etag:
self.add_error('etag', 'ETag错误')
cos_length = result.get('Content-Length')
if int(cos_length) != size:
self.add_error('size', '文件大小错误')
return self.cleaned_data
```

### 7.5.7 数据校验完写入数据库&页面展示
```html
名称
文件大小
更新者
更新时间
操作
{% for item in file_object_list %}
{% if item.file_type == 1 %}
{{ item.name }} {% else %}
{{ item.name }}
{% endif %}
{% if item.file_type == 1 %} {{ item.file_size }} {% else %} - {% endif %}
{{ item.update_user.username }}
{{ item.update_datetime }}
{% if item.file_type == 2 %}
{% endif %}
{% endfor %}
```
```javascript
Body: fileObject, // 上传文件对象
onProgress: function(progressData) {
console.log("文件上传进度---------->",fileName,JSON.stringify(progressData));
var percent = progressData.percent * 100 + '%';
tr.find('.progress-bar').text(percent);
tr.find('.progress-bar').css('width', percent);
}
}, function(err, data) {
console.log(err || data)
if (data && data.statusCode == 200){
// 上传成功,将本次上传文件信息发送后台校验并写入数据库
// 当前文件上传成功
$.post(FILE_POST,{
name:fileName,
file_size:fileSize,
key:key,
parent: CURRENT_FOLDER_ID,
etag:data.ETag,
file_path: data.Location
},function (res) {
console.log(res);
// 在数据库中写入成功,将已添加的数据在页面动态展示
console.log('res------------->',res);
var newTr = $('#rowTpl').find('tr').clone();
newTr.find('.name').text(res.data.name);
newTr.find('.file_size').text(res.data.file_size);
newTr.find('.username').text(res.data.username);
newTr.find('.datetime').text(res.data.datetime);
newTr.find('.delete').attr('data-fid', res.data.id);
//newTr.find('.download').attr('href', res.data.download_url);
$('#rowList').append(newTr);
// 自己的进度删除
tr.remove();
//location.href = location.href;
})
} else {
tr.find('.progress-error').text('上传失败');
}
});
})
});
}
```
```
@csrf_exempt
def file_post(request,project_id):
"""
已上传成功的文件属性校验&写入数据库
name:fileName,
file_size:fileSize,
key:key,
parent: CURRENT_FOLDER_ID,
etag:data.ETag,
file_path: data.Location
"""
# print(request.POST)
form = FileModelForm(request,data=request.POST)
if form.is_valid():
# 通过ModelForm.save存储到数据库中的数据返回的isntance对象,无法通过get_xx_display获取choice的中文
# form.instance.file_type = 1
# form.update_user = request.saas.user
# instance = form.save() # 添加成功之后,获取到新添加的那个对象(instance.id,instance.name,instance.file_type,instace.get_file_type_display()
# 校验通过:数据写入到数据库
data_dict = form.cleaned_data
data_dict.pop('etag')
data_dict.update({'project': request.saas.project, 'file_type': 1, 'update_user': request.saas.user})
instance = models.FileRepository.objects.create(**data_dict)
# 项目的已使用空间:更新 (data_dict['file_size'])
request.saas.project.use_space += data_dict['file_size']
request.saas.project.save()
result = {
'id': instance.id,
'name': instance.name,
'file_size': instance.file_size,
'username': instance.update_user.username,
'datetime': instance.update_datetime.strftime('%Y/%m/%d %H:%M'),
# 'download_url': reverse('file_download', kwargs={"project_id": project_id, 'file_id': instance.id})
# 'file_type': instance.get_file_type_display()
}
return JsonResponse({'status': True, 'data': result})
return JsonResponse({'status': False, 'data': "文件错误"})
```
## 7.6 文件下载
## 7.6.2 下载文件思路
```
用户浏览器 django服务器
请求 HttpResponse() 文本+响应头
请求 render() 文本+响应头
请求 ... 文本+响应头
```

```python
def download(request):
# 打开文件,读取文件内容
with open('xxx.png','rb') as f:
f.read()
response = HttpResponse(data)
# 设置响应头
response['Content-Disposition'] = "attachment;filename=xxx.png"
return response
```
### 7.6.3 下载实现
```html
{% endblock %} {% block content %}
名称
文件大小
更新者
更新时间
操作
{% for item in file_object_list %}
{% if item.file_type == 1 %}
{{ item.name }} {% else %}
{{ item.name }}
{% endif %}
{% if item.file_type == 1 %} {{ item.file_size }} {% else %} - {% endif %}
{{ item.update_user.username }}
{{ item.update_datetime }}
{% if item.file_type == 2 %}
{% else %}
{% endif %}
{% endfor %}
```
## 7.7 删除项目
# 八、问题管理模块
## 8.1 设计表结构
```python
# 产品经理:功能 + 原型图
# 开发人员: 表结构设计
```
| ID | title | desc | issue_id | module_id | status | priority | executor | 关注者 | s_time | e_time | 模式 | 父问题 |
| ---- | ----- | ---- | -------- | --------- | ------ | -------- | -------- | :----: | ------ | ------ | ---- | ------ |
| | | | | | | | | | | | | |
| | | | | | | | | | | | | |
| | | | | | | | | | | | | |
| ID | 问题类型 | 项目ID |
| ---- | -------- | ------ |
| 1 | bug | |
| 2 | 任务 | |
| | 功能 | |
| ID | 模块 | 项目ID |
| ---- | --------------- | ------ |
| 1 | 第一期 用户模块 | |
| 2 | 第二期 任务模块 | |
| | 第三期 支付模块 | |
```python
class Module(models.Model):
"""模块(里程碑)"""
project = models.ForeignKey(verbose_name='项目',to='Project',on_delete=models.CASCADE)
title = models.CharField(verbose_name='模块名称',max_length=32)
def __str__(self):
return self.title
class IssuesType(models.Model):
"""问题类型"""
'''
PROJECT_INIT_LIST = [
{'title':'任务','color':4},
{'title':'功能','color':1},
{'title':'Bug','color':2}
]
'''
title = models.CharField(verbose_name='类型名称',max_length=32)
# color = models.SmallIntegerField(verbose_name='颜色',choices=PROJECT_INIT_LIST,default=1)
project = models.ForeignKey(verbose_name='项目',to='Project',on_delete=models.CASCADE)
def __str__(self):
return self.title
class Issues(models.Model):
""" 问题 """
project = models.ForeignKey(verbose_name='项目', to='Project',on_delete=models.CASCADE)
issues_type = models.ForeignKey(verbose_name='问题类型', to='IssuesType')
module = models.ForeignKey(verbose_name='模块', to='Module', null=True, blank=True,on_delete=models.CASCADE)
subject = models.CharField(verbose_name='主题', max_length=80)
desc = models.TextField(verbose_name='问题描述')
priority_choices = (
("danger", "高"),
("warning", "中"),
("success", "低"),
)
priority = models.CharField(verbose_name='优先级', max_length=12, choices=priority_choices, default='danger')
# 新建、处理中、已解决、已忽略、待反馈、已关闭、重新打开
status_choices = (
(1, '新建'),
(2, '处理中'),
(3, '已解决'),
(4, '已忽略'),
(5, '待反馈'),
(6, '已关闭'),
(7, '重新打开'),
)
status = models.SmallIntegerField(verbose_name='状态', choices=status_choices, default=1)
assign = models.ForeignKey(verbose_name='指派', to='UserInfo', related_name='task', null=True, blank=True,on_delete=models.CASCADE)
attention = models.ManyToManyField(verbose_name='关注者', to='UserInfo', related_name='observe', blank=True)
start_date = models.DateField(verbose_name='开始时间', null=True, blank=True)
end_date = models.DateField(verbose_name='结束时间', null=True, blank=True)
mode_choices = (
(1, '公开模式'),
(2, '隐私模式'),
)
mode = models.SmallIntegerField(verbose_name='模式', choices=mode_choices, default=1)
parent = models.ForeignKey(verbose_name='父问题', to='self', related_name='child', null=True, blank=True,
on_delete=models.SET_NULL)
creator = models.ForeignKey(verbose_name='创建者', to='UserInfo', related_name='create_problems',on_delete=models.CASCADE)
create_datetime = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
latest_update_datetime = models.DateTimeField(verbose_name='最后更新时间', auto_now=True)
def __str__(self):
return self.subject
```
## 8.2 新建问题
### 8.2.1 点击新建问题单出对话框
- 显示对话框
- 显示用户要填写的数据(表单)
- 显示时间的前端插件
- bootstrap-datepicker网址: https://bootstrap-datepicker.readthedocs.io/en/latest/
- 显示下拉框插件:
- bootstrap-select插件: https://www.bootstrapselect.cn/
```python
# 1. 引入jS,CSS
# 2.ModelForm中添加属性
```
### 8.2.2 添加问题
#### 8.2.2.1 数据初始化&合法性
```python
# DjangoForm表单中进行验证
class IssuesModelForm(BootStrapForm,forms.ModelForm):
class Meta:
model = models.Issues
exclude = ['project','creator','create_datetime','latest_update_datetiem']
widgets = {
"assign": forms.Select(attrs={'class': "selectpicker", "data-live-search": "true"}),
"attention": forms.SelectMultiple(
attrs={'class': "selectpicker", "data-live-search": "true", "data-actions-box": "true"}),
"parent": forms.Select(attrs={'class': "selectpicker", "data-live-search": "true"}),
"start_date": forms.DateTimeInput(attrs={'autocomplete': "off"}),
"end_date": forms.DateTimeInput(attrs={'autocomplete': "off"})
}
def __init__(self,request,*args,**kwargs):
super().__init__(*args,**kwargs)
# 处理数据初始化
# 1.获取当前项目的所有问题类型 [(1,'xx'),(2,"xx")]
self.fields['issues_type'].choices = models.IssuesType.objects.filter(
project=request.saas.project).values_list('id', 'title')
# 2.获取当前项目的所有模块
module_list = [("", "没有选中任何项"), ]
module_object_list = models.Module.objects.filter(project=request.saas.project).values_list('id', 'title')
module_list.extend(module_object_list)
self.fields['module'].choices = module_list
# 3.指派和关注者
# 数据库找到当前项目的参与者 和 创建者
total_user_list = [(request.saas.project.creator_id, request.saas.project.creator.username), ]
project_user_list = models.ProjectUser.objects.filter(project=request.saas.project).values_list('user_id',
'user__username')
total_user_list.extend(project_user_list)
self.fields['assign'].choices = [("", "没有选中任何项")] + total_user_list
self.fields['attention'].choices = total_user_list
# 4. 当前项目已创建的问题
parent_list = [("", "没有选中任何项")]
parent_object_list = models.Issues.objects.filter(project=request.saas.project).values_list('id', 'subject')
parent_list.extend(parent_object_list)
self.fields['parent'].choices = parent_list
```
#### 8.2.2.2 添加数据(成功之后页面刷新)
#### 8.2.2.3 错误提示信息
## 8.3 问题列表
## 8.4 自定义分页
```python
http://127.0.0.1:8002/web/manage/issues/?Page=1/
http://127.0.0.1:8002/web/manage/issues/?Page=2/
# 1.分页
models.Issues.objects.all()[0:10]
models.Issues.objects.all()[0:20]
...
# 2.显示页码
- 当前访问的页码高亮显示
- 显示11页(前5页后5页,中间省略号)
```
```python
"""
分页组件应用:
1. 在视图函数中
queryset = models.Issues.objects.filter(project_id=project_id)
page_object = Pagination(
current_page=request.GET.get('page'),
all_count=queryset.count(),
base_url=request.path_info,
query_params=request.GET
)
issues_object_list = queryset[page_object.start:page_object.end]
context = {
'issues_object_list': issues_object_list,
'page_html': page_object.page_html()
}
return render(request, 'issues.html', context)
2. 前端
{% for item in issues_object_list %}
{{item.xxx}}
{% endfor %}
"""
class Pagination(object):
def __init__(self, current_page, all_count, base_url, query_params, per_page=30, pager_page_count=11):
"""
分页初始化
:param current_page: 当前页码
:param per_page: 每页显示数据条数
:param all_count: 数据库中总条数
:param base_url: 基础URL
:param query_params: QueryDict对象,内部含所有当前URL的原条件
:param pager_page_count: 页面上最多显示的页码数量
"""
self.base_url = base_url
try:
self.current_page = int(current_page)
if self.current_page <= 0:
self.current_page = 1
except Exception as e:
self.current_page = 1
query_params = query_params.copy()
query_params._mutable = True
self.query_params = query_params
self.per_page = per_page
self.all_count = all_count
self.pager_page_count = pager_page_count
pager_count, b = divmod(all_count, per_page)
if b != 0:
pager_count += 1
self.pager_count = pager_count
half_pager_page_count = int(pager_page_count / 2)
self.half_pager_page_count = half_pager_page_count
@property
def start(self):
"""
数据获取值起始索引
:return:
"""
return (self.current_page - 1) * self.per_page
@property
def end(self):
"""
数据获取值结束索引
:return:
"""
return self.current_page * self.per_page
def page_html(self):
"""
生成HTML页码
:return:
"""
if self.all_count == 0:
return ""
# 如果数据总页码pager_count<11 pager_page_count
if self.pager_count < self.pager_page_count:
pager_start = 1
pager_end = self.pager_count
else:
# 数据页码已经超过11
# 判断: 如果当前页 <= 5 half_pager_page_count
if self.current_page <= self.half_pager_page_count:
pager_start = 1
pager_end = self.pager_page_count
else:
# 如果: 当前页+5 > 总页码
if (self.current_page + self.half_pager_page_count) > self.pager_count:
pager_end = self.pager_count
pager_start = self.pager_count - self.pager_page_count + 1
else:
pager_start = self.current_page - self.half_pager_page_count
pager_end = self.current_page + self.half_pager_page_count
page_list = []
if self.current_page <= 1:
prev = '上一页 '
else:
self.query_params['page'] = self.current_page - 1
prev = '上一页 ' % (self.base_url, self.query_params.urlencode())
page_list.append(prev)
for i in range(pager_start, pager_end + 1):
self.query_params['page'] = i
if self.current_page == i:
tpl = '%s ' % (
self.base_url, self.query_params.urlencode(), i,)
else:
tpl = '%s ' % (self.base_url, self.query_params.urlencode(), i,)
page_list.append(tpl)
if self.current_page >= self.pager_count:
nex = '下一页 '
else:
self.query_params['page'] = self.current_page + 1
nex = '下一页 ' % (self.base_url, self.query_params.urlencode(),)
page_list.append(nex)
if self.all_count:
tpl = "共%s条数据,页码%s/%s页 " % (
self.all_count, self.current_page, self.pager_count,)
page_list.append(tpl)
page_str = "".join(page_list)
return page_str
```
# 九、邀请模块
# 十、概览模块
# 十一、支付模块
### 11.1 思路
- 点击价格展示套餐(无需登录就能查看)
- 选完套餐弹出支付页面(需要登录之后才能看到)
- 点击立即支持,调用支付宝接口,跳转到扫码支付页面
- 订单列表页面(支付超时、取消订单、再次发起支付等)
## 11.2 支付宝支付API
正式上线环境:个人公司或营业执照(收费)
沙箱环境(模拟开发环境,免费): https://opendocs.alipay.com/open/200/105311
### 11.2.1 申请开通沙箱环境
操作指导: https://openhome.alipay.com/platform/appDaily.htm
**1.注册成功之后会获取两个值:**
- APPID: 2016102700771267
- 支付宝网关(沙箱环境): https://openapi.alipaydev.com/gateway.do
正式线上环境: https://openapi.alipay.com/gateway.do
**2.生成秘钥**
功能:秘钥用于对URL中添加的参数进行加密和校验
- 下载秘钥生成器
- 生成一对:应用公钥;应用私钥

- 根据操作指导安装秘钥生成器: https://opendocs.alipay.com/open/291/106097/

**3.上传应用公钥并获得支付公钥**


本次操作共获得三个秘钥:
- 应用公钥
- 应用私钥,对以后URL中传入的数据进行签名加密用
- 支付宝公钥(通过应用公钥生成),在支付宝支付成功后跳转回来的时候。对支付宝给我们传的值进行校验
**4.账户信息和测试APP**


## 11.3 支付宝参数
**开发支持两种方式:**
- SDK,写好一个python模块
SDK接口开发网址: https://opendocs.alipay.com/open/54/103419
```python
# 1.安装模块
# 2.基于模块实现想要的功能
```
```python
pip install alipay-sdk-python==3.3.398
# 不推荐(python SDK不成熟,代码比较老旧)
```
- API,就是给开发者提高一个URL
API接口开发文档: https://opendocs.alipay.com/apis/api_1/alipay.trade.page.pay
```python
# 1.自己动手给URL进行处理和加密
```

```python
# API接口本质:让你跳转到: https://openapi.alipaydev.com/gateway.do/?参数组成
# 网关: https://openapi.alipaydev.com/gateway.do
# 参数: API文档中所涉及的公共参数
# 参数示例:
params = {
app_id:'',
method:'alipay.trade.page.pay', # 接口名称
format:'JSON',
return_url:'支付成功之后跳转(GET)到的哪个页面地址',
notify_url:'同时偷偷向这个地址发送一个POST请求',
charset:'utf-8',
sign_type:'RSA2', # 加密类型
sign:'', # 签名
timestamp:'2014-07-24 03:07:50', # 当前时间格式
version:'1.0', # 调用的接口版本,固定为:1.0
biz_content:{
out_trade_no:'订单号',
product_code:'FAST_INSTANT_TRADE_PAY',
total_amount:88.88, # 订单总金额,单位为元
subject:'Iphone6 16G',# 订单标题
}, # 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档
}
```
```python
# 如果支付成功,服务器宕机,如何处理?
#对于 PC 网站支付的交易,在用户支付完成之后,支付宝会根据 API 中商户传入的 notify_url,通过 POST 请求的形式将支付结果作为参数通知到商户系统。
# 偷偷向notify_url发送请求,说支付成功,你更改一下状态。
# 服务器宕机,支付包访问不到,则会在24小时内
# 程序执行完后必须打印输出“success”(不包含引号)。如果商户反馈给支付宝的字符不是 success 这7个字符,支付宝服务器会不断重发通知,直到超过24小时22分钟。一般情况下,25小时以内完成8次通知(通知的间隔频率一般是:4m,10m,10m,1h,2h,6h,15h);
# 异步支付:https://opendocs.alipay.com/open/270/105902
```
## 11.4 签名原理
支付包签名的过程:对参数进行处理,处理完之后再让它和网关拼接起来
签名文档: https://opendocs.alipay.com/open/291/105974
自行实现签名: https://opendocs.alipay.com/open/291/106118
```python
params = {
app_id:'',
method:'alipay.trade.page.pay', # 接口名称
format:'JSON',
return_url:'支付成功之后跳转(GET)到的哪个页面地址',
notify_url:'同时偷偷向这个地址发送一个POST请求',
charset:'utf-8',
sign_type:'RSA2', # 加密类型
sign:'', # 签名
timestamp:'2014-07-24 03:07:50', # 当前时间格式
version:'1.0', # 调用的接口版本,固定为:1.0
biz_content:{
out_trade_no:'订单号',
product_code:'FAST_INSTANT_TRADE_PAY',
total_amount:88.88, # 订单总金额,单位为元
subject:'Iphone6 16G',# 订单标题
}, # 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档
}
# 1.筛选并排序
# 获取所有请求参数,不包括字节类型参数,如文件、字节流,剔除 sign 字段,剔除值为空的参数,并按照第一个字符的键值 ASCII 码递增排序(字母升序排序),如果遇到相同字符则按照第二个字符的键值 ASCII 码递增排序,以此类推。
params.pop(sign)
sort(params[key])
# 2. 拼接
# 将排序后的参数与其对应值,组合成“参数=参数值”的格式,并且把这些参数用 & 字符连接起来,此时生成的字符串为待签名字符串。
待签名的字符串 = "app_id=xxx&method=alipay.trade.page.pay"
# 注意:1.有字典应该转换为字符串2.字符串中间不能有空格(处理方式如下图)
# 3.使用各自语言对应的 SHA256WithRSA(对应sign_type为RSA2)或SHA1WithRSA(对应sign_type为RSA)签名函数并利用商户私钥(即应用私钥)对待签名字符串进行签名,并进行 Base64 编码。
# 注意:base64编码之后内部不能有换行符
result = SHA256WithRSA签名和私钥对签名字符串进行签名
签名 = 再对result进行Base64编码.replace("\n","")
把签名再添加回params字典中:params[sign] = 签名
# 4.再将所有的参数拼接起来
# 注意:在拼接URL的时候不能出现, ; (等特殊字符,提前将特殊字符转换URL转义的字符
# 可以借助Python第三方模块实现URL中特殊字符转义
from urllib.parse import quote_plus
```

## 11.5 签名实现
```python
# 安装加密模块
pip install pycrypto
# 报错解决方案,安装替代模块:pycryptodom
```
```python
# 构造字典
params = {
app_id:'',
method:'alipay.trade.page.pay', # 接口名称
format:'JSON',
return_url:'支付成功之后跳转(GET)到的哪个页面地址',
notify_url:'同时偷偷向这个地址发送一个POST请求',
charset:'utf-8',
sign_type:'RSA2', # 加密类型
sign:'', # 签名
timestamp:'2014-07-24 03:07:50', # 当前时间格式
version:'1.0', # 调用的接口版本,固定为:1.0
biz_content:{
out_trade_no:'订单号',
product_code:'FAST_INSTANT_TRADE_PAY',
total_amount:88.88, # 订单总金额,单位为元
subject:'Iphone6 16G',# 订单标题
}, separators=(',',':')
# 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档
}
# 获取待签名的字符串
unsigned_string = "&".join(["{0}={1}".format(k,params[k]) for k in sorted(params)])
print(unsigned_string)
# 签名
from Crypto.Publickey import RSA
from Crypto.Signature import PKCS1_V1_5
from Crypto.Hash import SHA256
from base64 import decodebytes,encodebytes
# SHA256WithRSA + 应用公钥 对待签名的字符串 进行签名
private_key = RSA.import(open('files/应用公钥2048.txt').read())
signer = PKCS1_V1_5.new(private_key)
signature = signer.sign(SHA256.new(unsigned_string.encode('utf-8')))
# base64编码 转换为字符串
sign_string = encodebytes(signature).decode('utf-8').replace('\n','')
# 把生成的签名赋值给sign参数,拼接到请求参数中
from urllib.parse import quote_plus
result = "&".join(["{0}={1}".format(k,quote_plus(params[k])) for k in sorted(params)])
result = result + "&sign=" + quote_plus(sign_string)
gateway = "https://openapi.alipaydev.com/gateway.do"
pay_url = "{}?{}".fromat(gateway,result)
return pay_url
```
# 十二、项目开发流程
- 需求整理
- 功能设计
- 表结构设计
- 开发,代码都是运行在自己电脑上的
- 前端
- 后端
- 运维:把代码部署到服务器上
- 租一台电脑 + 租一个公网IP + 租域名
- 域名解析 : 域名 - IP绑定 - 电脑(django)
- Linux操作系统:Linux操作系统上部署环境(安装python、Django、redis)
- Linux代码部署: nginx + uwsgi
- docker中安装部署python 、django、redis运行部署项目