├── .DS_Store ├── .gitignore ├── .vscode └── settings.json ├── flask ├── README.md ├── server.py └── server1.py ├── ftp ├── abc.txt └── ftp.py ├── numpy ├── function.py ├── index.js ├── loop.py ├── numpy.py └── test.py ├── opencv ├── canny.py ├── drive │ ├── adb │ │ ├── AdbNativeMessaging.exe │ │ ├── AdbNativeMessaging.exe.config │ │ ├── AdbWinApi.dll │ │ ├── AdbWinUsbApi.dll │ │ ├── BouncyCastle.Crypto.dll │ │ ├── Newtonsoft.Json.dll │ │ ├── UniversalADB.cer │ │ ├── UniversalAdbDriverInstaller.exe │ │ ├── UniversalAdbDriverInstaller.exe.config │ │ ├── UniversalAdbDriverInstaller.exe.manifest │ │ ├── adb.exe │ │ ├── makecert.exe │ │ ├── nmh-manifest.json │ │ ├── signtool.exe │ │ └── usb_driver │ │ │ ├── amd64 │ │ │ ├── NOTICE.txt │ │ │ ├── WUDFUpdate_01007.dll │ │ │ ├── WUDFUpdate_01009.dll │ │ │ ├── WdfCoInstaller01007.dll │ │ │ ├── WdfCoInstaller01009.dll │ │ │ ├── winusbcoinstaller.dll │ │ │ └── winusbcoinstaller2.dll │ │ │ ├── android_winusb.inf │ │ │ ├── androidwinusb86.cat │ │ │ ├── androidwinusba64.cat │ │ │ ├── i386 │ │ │ ├── NOTICE.txt │ │ │ ├── WUDFUpdate_01007.dll │ │ │ ├── WUDFUpdate_01009.dll │ │ │ ├── WdfCoInstaller01007.dll │ │ │ ├── WdfCoInstaller01009.dll │ │ │ ├── winusbcoinstaller.dll │ │ │ └── winusbcoinstaller2.dll │ │ │ └── source.properties │ ├── canny.py │ ├── deal.html │ ├── img │ │ ├── screen.png │ │ └── test.jpg │ ├── index.html │ ├── js │ │ ├── axios.js │ │ ├── jquery.js │ │ └── vue.js │ ├── package.json │ ├── test.py │ └── tyt.js ├── img │ └── test.jpg ├── index.html ├── js │ ├── axios.js │ └── vue.js ├── package-lock.json ├── test.js └── test.py ├── scrapy ├── download.py ├── download.txt ├── get.py ├── post.py ├── test.php └── test.txt ├── tutorial ├── .DS_Store ├── install │ └── README.md └── swiper │ ├── .gitignore │ ├── README.md │ ├── TODO.md │ ├── backend │ ├── common │ │ ├── __init__.py │ │ ├── errors.py │ │ ├── keys.py │ │ ├── middleware.py │ │ └── utils.py │ ├── lib │ │ ├── __init__.py │ │ ├── cache.py │ │ ├── db.py │ │ ├── http.py │ │ ├── mail.py │ │ ├── qiniu.py │ │ └── sms.py │ ├── manage.py │ ├── social │ │ ├── __init__.py │ │ ├── api.py │ │ ├── apps.py │ │ ├── logic.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ └── models.py │ ├── swiper │ │ ├── __init__.py │ │ ├── gunicorn_config.py │ │ ├── platform_config.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── user │ │ ├── __init__.py │ │ ├── api.py │ │ ├── apps.py │ │ ├── forms.py │ │ ├── logic.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ └── models.py │ ├── vip │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── api.py │ │ ├── apps.py │ │ ├── logic.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ └── models.py │ └── worker │ │ ├── __init__.py │ │ └── config.py │ ├── chat │ ├── application.py │ ├── config.py │ ├── django_env.py │ ├── handler.py │ ├── log.py │ └── logic.py │ ├── deployment │ ├── README.md │ ├── celery-start.sh │ ├── init.py │ ├── nginx.conf │ ├── release.sh │ ├── restart.sh │ ├── setup.sh │ ├── start.sh │ └── stop.sh │ ├── doc │ ├── 4.1.1-团队构建及项目管理.md │ ├── 4.1.2-User功能开发.md │ ├── 4.1.3-个人资料功能开发.md │ ├── 4.1.4-Social模块开发-1.md │ ├── 4.1.5-Social模块开发-2.md │ ├── 4.2.1-VIP模块开发及日志处理.md │ ├── 4.2.2-缓存及NoSQL的使用.md │ ├── 4.2.3-分布式数据库及性能评估.md │ ├── 4.2.4-上线部署及脚本开发.md │ ├── 4.2.5-服务器架构.md │ ├── chat_api.md │ ├── img │ │ ├── CDN.png │ │ ├── GIL.png │ │ ├── arch-1.jpg │ │ ├── arch-10.jpg │ │ ├── arch-2.jpg │ │ ├── arch-3.jpg │ │ ├── arch-4.jpg │ │ ├── arch-5.jpg │ │ ├── arch-6.jpg │ │ ├── arch-7.jpg │ │ ├── arch-8.jpg │ │ ├── arch-9.jpg │ │ ├── celery.png │ │ ├── fb-dev.jpg │ │ ├── front-back.jpg │ │ ├── git.png │ │ ├── master-slave-1.png │ │ └── master-slave-2.png │ └── web_api.md │ ├── frontend │ ├── css │ │ └── pure-min.css │ ├── img │ │ ├── help.png │ │ ├── history.png │ │ ├── icon.png │ │ ├── like-txt.png │ │ ├── like.png │ │ ├── nope-txt.png │ │ ├── nope.png │ │ ├── super-like.png │ │ └── super-txt.png │ ├── info.html │ ├── js │ │ ├── axios.min.js │ │ ├── init.js │ │ ├── qs.js │ │ ├── vue-swiper.js │ │ └── vue.js │ ├── login.html │ └── swiper.html │ └── requirements.txt └── zip ├── test.txt ├── test.zip └── zip.py /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /flask/README.md: -------------------------------------------------------------------------------- 1 | ```py 2 | if __name__ == '__main__': 3 | app.run() 4 | ``` 5 | 6 | 最后我们用 run() 函数来让应用运行在本地服务器上。 其中 if __name__ == '__main__': 确保服务器只会在该脚本被 Python 解释器直接执行的时候才会运行,而不是作为模块导入的时候 -------------------------------------------------------------------------------- /flask/server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | app = Flask(__name__) 3 | 4 | @app.route('/') 5 | def hello_world(): 6 | return 'Hello World!' 7 | 8 | @app.route('/hello') 9 | def hello(): 10 | return 'wscats' 11 | 12 | @app.route('/user/') 13 | def show_user_profile(username): 14 | # show the user profile for that user 15 | return 'User %s' % username 16 | 17 | # 确保服务器只会在该脚本被 Python 解释器直接执行的时候才会运行 18 | if __name__ == '__main__': 19 | app.run() -------------------------------------------------------------------------------- /flask/server1.py: -------------------------------------------------------------------------------- 1 | from flask import Flask,url_for 2 | app = Flask(__name__) 3 | 4 | @app.route('/') 5 | def hello_world(): 6 | return 'Hello World!' 7 | 8 | @app.route('/hello') 9 | def hello(): 10 | return 'wscats' 11 | 12 | @app.route('/user/') 13 | def show_user_profile(username): 14 | # show the user profile for that user 15 | return 'User %s' % username 16 | 17 | # static file 18 | with app.test_request_context(): 19 | print url_for('index') 20 | print url_for('login') 21 | print url_for('login', next='/') 22 | print url_for('profile', username='John Doe') 23 | 24 | if __name__ == '__main__': 25 | app.run() -------------------------------------------------------------------------------- /ftp/abc.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/ftp/abc.txt -------------------------------------------------------------------------------- /ftp/ftp.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | from ftplib import FTP 3 | #设置变量 4 | ftp = FTP() 5 | 6 | timeout = 30 7 | port = 21 8 | 9 | # 连接FTP服务器 10 | ftp.connect('yuanxiaobo.gotoftp5.com',port,timeout) 11 | # 登录 12 | ftp.login('yuanxiaobo','19870616yxb') 13 | 14 | # 获得欢迎信息 15 | print ftp.getwelcome() 16 | 17 | # 获取目录下的文件,获得目录列表 18 | list = ftp.nlst() 19 | for name in list: 20 | print name 21 | 22 | # 定义文件保存路径 23 | name = 'abc.txt' 24 | path = './' + name 25 | # 打开要保存文件 26 | f = open(path,'wb') 27 | # 保存FTP文件 28 | filename = 'RETR ' + name 29 | # 保存FTP上的文件 30 | ftp.retrbinary(filename,f.write) 31 | 32 | # 删除FTP文件 33 | # ftp.delete(name) 34 | 35 | # 上传FTP文件 36 | # ftp.storbinary('STOR test.txt', open(path, 'rb')) 37 | # ftp.quit() -------------------------------------------------------------------------------- /numpy/function.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | #arr = np.arange(14).reshape(2, 7) 3 | #arr = np.arange(15) 4 | #arr = np.zeros([3,4,6]) 5 | arr = np.random.random((2, 2, 3)) 6 | #arr = np.random.rand(2, 2, 3) 7 | #arr = np.random.rand(2, 2, 3) 8 | #arr = np.ones((2,3,4), dtype=np.int32) 9 | #arr = np.random.randint(0, 2, 10) 10 | print(arr) 11 | -------------------------------------------------------------------------------- /numpy/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {number[]} nums 3 | * @param {number} target 4 | * @return {number[]} 5 | */ 6 | var twoSum = function (nums, target) { 7 | for (let a = 0; a < nums.length; a++) { 8 | console.log(a) 9 | for (let b = 0; b < a; b++) { 10 | console.log(b) 11 | } 12 | } 13 | }; 14 | twoSum([2, 4, 6, 9], 15) 15 | 16 | // [0,1] 17 | // [0,2] 18 | // [0,3] 19 | // [1,2] 20 | // [1,3] 21 | // [2,3] -------------------------------------------------------------------------------- /numpy/loop.py: -------------------------------------------------------------------------------- 1 | animals = ['cat', 'dog', 'monkey'] 2 | for animal in animals: 3 | print(animal) 4 | 5 | animals = ['cat', 'dog', 'monkey'] 6 | for idx, animal in enumerate(animals): 7 | print(idx + 1, animal) 8 | -------------------------------------------------------------------------------- /numpy/numpy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import numpy; 4 | import matplotlib.pyplot as plt 5 | import pylab 6 | 7 | print '使用列表生成一维数组' 8 | data = [1,2,3,4,5,6] 9 | x = numpy.array(data) 10 | print x #打印数组 11 | print x.dtype #打印数组元素的类型 12 | 13 | print '使用列表生成二维数组' 14 | data = [[1,2],[3,4],[5,6]] 15 | x = numpy.array(data) 16 | print x #打印数组 17 | print x.ndim #打印数组的维度 18 | print x.shape #打印数组各个维度的长度。shape是一个元组 19 | 20 | print '使用zero/ones/empty创建数组:根据shape来创建' 21 | x = numpy.zeros(6) #创建一维长度为6的,元素都是0一维数组 22 | print x 23 | x = numpy.zeros((2,3)) #创建一维长度为2,二维长度为3的二维0数组 24 | print x 25 | x = numpy.ones((2,3)) #创建一维长度为2,二维长度为3的二维1数组 26 | print x 27 | x = numpy.empty((3,3)) #创建一维长度为2,二维长度为3,未初始化的二维数组 28 | print x 29 | 30 | print '使用arrange生成连续元素' 31 | print numpy.arange(6) # [0,1,2,3,4,5,] 开区间 32 | print numpy.arange(0,6,2) # [0, 2,4] 33 | 34 | print '生成指定元素类型的数组:设置dtype属性' 35 | x = numpy.array([1,2.6,3],dtype = numpy.int64) 36 | print x # 元素类型为int64 37 | print x.dtype 38 | x = numpy.array([1,2,3],dtype = numpy.float64) 39 | print x # 元素类型为float64 40 | print x.dtype 41 | 42 | print '使用astype复制数组,并转换类型' 43 | x = numpy.array([1,2.6,3],dtype = numpy.float64) 44 | y = x.astype(numpy.int32) 45 | print y # [1 2 3] 46 | print x # [ 1. 2.6 3. ] 47 | z = y.astype(numpy.float64) 48 | print z # [ 1. 2. 3.] 49 | 50 | print '将字符串元素转换为数值元素' 51 | x = numpy.array(['1','2','3'],dtype = numpy.string_) 52 | y = x.astype(numpy.int32) 53 | print x # ['1' '2' '3'] 54 | print y # [1 2 3] 若转换失败会抛出异常 55 | 56 | print '使用其他数组的数据类型作为参数' 57 | x = numpy.array([ 1., 2.6,3. ],dtype = numpy.float32); 58 | y = numpy.arange(3,dtype=numpy.int32); 59 | print y # [0 1 2] 60 | print y.astype(x.dtype) # [ 0. 1. 2.] 61 | 62 | 63 | print 'ndarray数组与标量/数组的运算' 64 | x = numpy.array([1,2,3]) 65 | print x*2 # [2 4 6] 66 | print x>2 # [False False True] 67 | y = numpy.array([3,4,5]) 68 | print x+y # [4 6 8] 69 | print x>y # [False False False] 70 | 71 | print 'ndarray的基本索引' 72 | x = numpy.array([[1,2],[3,4],[5,6]]) 73 | print x[0] # [1,2] 74 | print x[0][1] # 2,普通python数组的索引 75 | print x[0,1] # 同x[0][1],ndarray数组的索引 76 | x = numpy.array([[[1, 2], [3,4]], [[5, 6], [7,8]]]) 77 | print x[0] # [[1 2],[3 4]] 78 | y = x[0].copy() # 生成一个副本 79 | z = x[0] # 未生成一个副本 80 | print y # [[1 2],[3 4]] 81 | print y[0,0] # 1 82 | y[0,0] = 0 83 | z[0,0] = -1 84 | print y # [[0 2],[3 4]] 85 | print x[0] # [[-1 2],[3 4]] 86 | print z # [[-1 2],[3 4]] 87 | 88 | print 'ndarray的切片' 89 | x = numpy.array([1,2,3,4,5]) 90 | print x[1:3] # [2,3] 右边开区间 91 | print x[:3] # [1,2,3] 左边默认为 0 92 | print x[1:] # [2,3,4,5] 右边默认为元素个数 93 | print x[0:4:2] # [1,3] 下标递增2 94 | x = numpy.array([[1,2],[3,4],[5,6]]) 95 | print x[:2] # [[1 2],[3 4]] 96 | print x[:2,:1] # [[1],[3]] 97 | x[:2,:1] = 0 98 | print x # [[0,2],[0,4],[5,6]] 99 | x[:2,:1] = [[8],[6]] 100 | print x # [[8,2],[6,4],[5,6]] 101 | 102 | print 'ndarray的布尔型索引' 103 | x = numpy.array([3,2,3,1,3,0]) 104 | # 布尔型数组的长度必须跟被索引的轴长度一致 105 | y = numpy.array([True,False,True,False,True,False]) 106 | print x[y] # [3,3,3] 107 | print x[y==False] # [2,1,0] 108 | print x>=3 # [ True False True False True False] 109 | print x[~(x>=3)] # [2,1,0] 110 | print (x==2)|(x==1) # [False True False True False False] 111 | print x[(x==2)|(x==1)] # [2 1] 112 | x[(x==2)|(x==1)] = 0 113 | print x # [3 0 3 0 3 0] 114 | 115 | print 'ndarray的花式索引:使用整型数组作为索引' 116 | x = numpy.array([1,2,3,4,5,6]) 117 | print x[[0,1,2]] # [1 2 3] 118 | print x[[-1,-2,-3]] # [6,5,4] 119 | x = numpy.array([[1,2],[3,4],[5,6]]) 120 | print x[[0,1]] # [[1,2],[3,4]] 121 | print x[[0,1],[0,1]] # [1,4] 打印x[0][0]和x[1][1] 122 | print x[[0,1]][:,[0,1]] # 打印01行的01列 [[1,2],[3,4]] 123 | # 使用numpy.ix_()函数增强可读性 124 | print x[numpy.ix_([0,1],[0,1])] #同上 打印01行的01列 [[1,2],[3,4]] 125 | x[[0,1],[0,1]] = [0,0] 126 | print x # [[0,2],[3,0],[5,6]] 127 | 128 | print 'ndarray数组的转置和轴对换' 129 | k = numpy.arange(9) #[0,1,....8] 130 | m = k.reshape((3,3)) # 改变数组的shape复制生成2维的,每个维度长度为3的数组 131 | print k # [0 1 2 3 4 5 6 7 8] 132 | print m # [[0 1 2] [3 4 5] [6 7 8]] 133 | # 转置(矩阵)数组:T属性 : mT[x][y] = m[y][x] 134 | print m.T # [[0 3 6] [1 4 7] [2 5 8]] 135 | # 计算矩阵的内积 xTx 136 | print numpy.dot(m,m.T) # numpy.dot点乘 137 | # 高维数组的轴对象 138 | k = numpy.arange(8).reshape(2,2,2) 139 | print k # [[[0 1],[2 3]],[[4 5],[6 7]]] 140 | print k[1][0][0] 141 | # 轴变换 transpose 142 | m = k.transpose((1,0,2)) # m[y][x][z] = k[x][y][z] 143 | print m # [[[0 1],[4 5]],[[2 3],[6 7]]] 144 | print m[0][1][0] 145 | # 轴交换 swapaxes (axes:轴) 146 | m = k.swapaxes(0,1) # 将第一个轴和第二个轴交换 m[y][x][z] = k[x][y][z] 147 | print m # [[[0 1],[4 5]],[[2 3],[6 7]]] 148 | print m[0][1][0] 149 | # 使用轴交换进行数组矩阵转置 150 | m = numpy.arange(9).reshape((3,3)) 151 | print m # [[0 1 2] [3 4 5] [6 7 8]] 152 | print m.swapaxes(1,0) # [[0 3 6] [1 4 7] [2 5 8]] 153 | 154 | print '一元ufunc示例' 155 | x = numpy.arange(6) 156 | print x # [0 1 2 3 4 5] 157 | print numpy.square(x) # [ 0 1 4 9 16 25] 158 | x = numpy.array([1.5,1.6,1.7,1.8]) 159 | y,z = numpy.modf(x) 160 | print y # [ 0.5 0.6 0.7 0.8] 161 | print z # [ 1. 1. 1. 1.] 162 | 163 | print '二元ufunc示例' 164 | x = numpy.array([[1,4],[6,7]]) 165 | y = numpy.array([[2,3],[5,8]]) 166 | print numpy.maximum(x,y) # [[2,4],[6,8]] 167 | print numpy.minimum(x,y) # [[1,3],[5,7]] 168 | 169 | print 'where函数的使用' 170 | cond = numpy.array([True,False,True,False]) 171 | x = numpy.where(cond,-2,2) 172 | print x # [-2 2 -2 2] 173 | cond = numpy.array([1,2,3,4]) 174 | x = numpy.where(cond>2,-2,2) 175 | print x # [ 2 2 -2 -2] 176 | y1 = numpy.array([-1,-2,-3,-4]) 177 | y2 = numpy.array([1,2,3,4]) 178 | x = numpy.where(cond>2,y1,y2) # 长度须匹配 179 | print x # [1,2,-3,-4] 180 | print 'where函数的嵌套使用' 181 | y1 = numpy.array([-1,-2,-3,-4,-5,-6]) 182 | y2 = numpy.array([1,2,3,4,5,6]) 183 | y3 = numpy.zeros(6) 184 | cond = numpy.array([1,2,3,4,5,6]) 185 | x = numpy.where(cond>5,y3,numpy.where(cond>2,y1,y2)) 186 | print x # [ 1. 2. -3. -4. -5. 0.] 187 | 188 | print 'numpy的基本统计方法' 189 | x = numpy.array([[1,2],[3,3],[1,2]]) #同一维度上的数组长度须一致 190 | print x.mean() # 2 191 | print x.mean(axis=1) # 对每一行的元素求平均 192 | print x.mean(axis=0) # 对每一列的元素求平均 193 | print x.sum() #同理 12 194 | print x.sum(axis=1) # [3 6 3] 195 | print x.max() # 3 196 | print x.max(axis=1) # [2 3 2] 197 | print x.cumsum() # [ 1 3 6 9 10 12] 198 | print x.cumprod() # [ 1 2 6 18 18 36] 199 | print '用于布尔数组的统计方法' 200 | x = numpy.array([[True,False],[True,False]]) 201 | print x.sum() # 2 202 | print x.sum(axis=1) # [1,1] 203 | print x.any(axis=0) # [True,False] 204 | print x.all(axis=1) # [False,False] 205 | print '.sort的就地排序' 206 | x = numpy.array([[1,6,2],[6,1,3],[1,5,2]]) 207 | x.sort(axis=1) 208 | print x # [[1 2 6] [1 3 6] [1 2 5]] 209 | #非就地排序:numpy.sort()可产生数组的副本 210 | 211 | print 'ndarray的唯一化和集合运算' 212 | x = numpy.array([[1,6,2],[6,1,3],[1,5,2]]) 213 | print numpy.unique(x) # [1,2,3,5,6] 214 | y = numpy.array([1,6,5]) 215 | print numpy.in1d(x,y) # [ True True False True True False True True False] 216 | print numpy.setdiff1d(x,y) # [2 3] 217 | print numpy.intersect1d(x,y) # [1 5 6] 218 | 219 | 220 | print 'ndarray的存取' 221 | x = numpy.array([[1,6,2],[6,1,3],[1,5,2]]) 222 | numpy.save('file1',x) #以二进制.npy保存 223 | numpy.savetxt('filetxt',x,delimiter=',') # 以文本保存 224 | y = numpy.load('file1.npy') 225 | print y # [[1 6 2] [6 1 3] [1 5 2]] 226 | y = numpy.loadtxt('filetxt',delimiter=',') # delimiter为分隔符 227 | print y # [[1 6 2] [6 1 3] [1 5 2]] 228 | # 压缩保存 229 | numpy.savez('filezip',a=x,b=y) 230 | print numpy.load('filezip.npz')['a'] # 按字典索引 [[1 6 2] [6 1 3] [1 5 2]] 231 | 232 | print '线性代数' 233 | import numpy.linalg as nla 234 | print '矩阵点乘' 235 | x = numpy.array([[1,2],[3,4]]) 236 | y = numpy.array([[1,3],[2,4]]) 237 | print x.dot(y) # [[ 5 11][11 25]] 238 | print numpy.dot(x,y) # # [[ 5 11][11 25]] 239 | print '矩阵求逆' 240 | x = numpy.array([[1,1],[1,2]]) 241 | y = nla.inv(x) # 矩阵求逆(若矩阵的逆存在) 242 | print x.dot(y) # 单位矩阵 [[ 1. 0.][ 0. 1.]] 243 | print nla.det(x) # 求行列式 244 | 245 | print 'numpy.random随机数生成' 246 | import numpy.random as npr 247 | 248 | x = npr.randint(0,2,size=100000) #抛硬币 249 | print (x>0).sum() # 正面的结果 250 | print npr.normal(size=(2,2)) #正态分布随机数数组 shape = (2,2) 251 | 252 | 253 | print 'ndarray数组重塑' 254 | x = numpy.arange(0,6) #[0 1 2 3 4] 255 | print x #[0 1 2 3 4] 256 | print x.reshape((2,3)) # [[0 1 2][3 4 5]] 257 | print x #[0 1 2 3 4] 258 | print x.reshape((2,3)).reshape((3,2)) # [[0 1][2 3][4 5]] 259 | y = numpy.array([[1,1,1],[1,1,1]]) 260 | x = x.reshape(y.shape) 261 | print x # [[0 1 2][3 4 5]] 262 | print x.flatten() # [0 1 2 3 4 5] 263 | x.flatten()[0] = -1 # flatten返回的是拷贝 264 | print x # [[0 1 2][3 4 5]] 265 | print x.ravel() # [0 1 2 3 4 5] 266 | x.ravel()[0] = -1 # ravel返回的是视图(引用) 267 | print x # [[-1 1 2][3 4 5]] 268 | print "维度大小自动推导" 269 | arr = numpy.arange(15) 270 | print arr.reshape((5, -1)) # 15 / 5 = 3 271 | 272 | print '数组的合并与拆分' 273 | x = numpy.array([[1, 2, 3], [4, 5, 6]]) 274 | y = numpy.array([[7, 8, 9], [10, 11, 12]]) 275 | print numpy.concatenate([x, y], axis = 0) # 竖直组合 [[ 1 2 3][ 4 5 6][ 7 8 9][10 11 12]] 276 | print numpy.concatenate([x, y], axis = 1) # 水平组合 [[ 1 2 3 7 8 9][ 4 5 6 10 11 12]] 277 | print '垂直stack与水平stack' 278 | print numpy.vstack((x, y)) # 垂直堆叠:相对于垂直组合 279 | print numpy.hstack((x, y)) # 水平堆叠:相对于水平组合 280 | # dstack:按深度堆叠 281 | print numpy.split(x,2,axis=0) # 按行分割 [array([[1, 2, 3]]), array([[4, 5, 6]])] 282 | print numpy.split(x,3,axis=1) # 按列分割 [array([[1],[4]]), array([[2],[5]]), array([[3],[6]])] 283 | 284 | print '数组的元素重复操作' 285 | x = numpy.array([[1,2],[3,4]]) 286 | print x.repeat(2) # 按元素重复 [1 1 2 2 3 3 4 4] 287 | print x.repeat(2,axis=0) # 按行重复 [[1 2][1 2][3 4][3 4]] 288 | print x.repeat(2,axis=1) # 按列重复 [[1 1 2 2][3 3 4 4]] 289 | x = numpy.array([1,2]) 290 | print numpy.tile(x,2) 291 | print numpy.tile(x, (2, 2)) # 指定从低位到高位依次复制的次数。 -------------------------------------------------------------------------------- /numpy/test.py: -------------------------------------------------------------------------------- 1 | #encoding=utf-8 2 | 3 | import numpy as np 4 | 5 | def main(): 6 | lst = [[1,2,3],[4,5,6]] 7 | print(type(lst)) #列表数据类型 8 | np_lst = np.array(lst) 9 | print(np_lst) #打印数组 10 | print(type(np_lst)) #经过numpy处理后的数组类型(矩阵) 11 | print(np_lst.shape) #打印数组各个维度的长度 12 | print(np_lst.ndim) #打印数组的维度 13 | print(np_lst.dtype) #打印数组元素的类型 14 | print(np_lst.itemsize) #打印每个字节长度 15 | print(np_lst.size) #打印数组长度 16 | print(np.array(lst, dtype=complex)) 17 | 18 | if __name__ == "__main__": 19 | main() -------------------------------------------------------------------------------- /opencv/canny.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | import cv2 3 | import numpy as np 4 | from json import dumps 5 | # 图片路径 6 | IMAGE_NAME = "img/test.jpg" 7 | # 保存为的json文件 8 | JSON_NAME = 'opencv_temp.json' 9 | img = cv2.imread(IMAGE_NAME) 10 | 11 | # numpy中ndarray文件转为list 12 | # img_list = img.tolist() 13 | img_list = img[:,:,::-1].tolist() 14 | # print(img_list) 15 | # 字典形式保存数组 16 | img_dict = {} 17 | img_dict['name'] = IMAGE_NAME 18 | img_dict['content'] = img_list 19 | 20 | # 保存为json格式 21 | # json_data = dumps(img_dict, indent=2) 22 | json_data = dumps(img_dict) 23 | 24 | # 将数据保存到文件 25 | with open(JSON_NAME, 'w') as json_file: 26 | json_file.write(json_data) 27 | -------------------------------------------------------------------------------- /opencv/drive/adb/AdbNativeMessaging.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/AdbNativeMessaging.exe -------------------------------------------------------------------------------- /opencv/drive/adb/AdbNativeMessaging.exe.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /opencv/drive/adb/AdbWinApi.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/AdbWinApi.dll -------------------------------------------------------------------------------- /opencv/drive/adb/AdbWinUsbApi.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/AdbWinUsbApi.dll -------------------------------------------------------------------------------- /opencv/drive/adb/BouncyCastle.Crypto.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/BouncyCastle.Crypto.dll -------------------------------------------------------------------------------- /opencv/drive/adb/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /opencv/drive/adb/UniversalADB.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/UniversalADB.cer -------------------------------------------------------------------------------- /opencv/drive/adb/UniversalAdbDriverInstaller.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/UniversalAdbDriverInstaller.exe -------------------------------------------------------------------------------- /opencv/drive/adb/UniversalAdbDriverInstaller.exe.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /opencv/drive/adb/UniversalAdbDriverInstaller.exe.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /opencv/drive/adb/adb.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/adb.exe -------------------------------------------------------------------------------- /opencv/drive/adb/makecert.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/makecert.exe -------------------------------------------------------------------------------- /opencv/drive/adb/nmh-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.clockworkmod.adb", 3 | "description": "Vysor", 4 | "path": "AdbNativeMessaging.exe", 5 | "type": "stdio", 6 | "allowed_origins": [ 7 | "chrome-extension:\/\/gidgenkbbabolejbgbpnhbimgjbffefm\/", 8 | "chrome-extension:\/\/njhehnieenekbompacofnhlljnobgcga\/", 9 | "chrome-extension:\/\/gpglbgbpeobllokpmeagpoagjbfknanl\/", 10 | "chrome-extension:\/\/kplbohaahpapodpbeolplkdkaddmlokj\/", 11 | "chrome-extension:\/\/ejlfdbijieaifbpalholclojlhhlabdc\/", 12 | "chrome-extension:\/\/pifcolcddlhpoafkkcelddpijgekcdgl\/" 13 | ] 14 | } -------------------------------------------------------------------------------- /opencv/drive/adb/signtool.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/signtool.exe -------------------------------------------------------------------------------- /opencv/drive/adb/usb_driver/amd64/NOTICE.txt: -------------------------------------------------------------------------------- 1 | The .dll files here are distributed by Microsoft Corporation as part of the 2 | Windows Driver Kit (available at 3 | http://www.microsoft.com/whdc/resources/downloads.mspx) and included here as 4 | permitted by the Microsoft Software License Terms. -------------------------------------------------------------------------------- /opencv/drive/adb/usb_driver/amd64/WUDFUpdate_01007.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/usb_driver/amd64/WUDFUpdate_01007.dll -------------------------------------------------------------------------------- /opencv/drive/adb/usb_driver/amd64/WUDFUpdate_01009.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/usb_driver/amd64/WUDFUpdate_01009.dll -------------------------------------------------------------------------------- /opencv/drive/adb/usb_driver/amd64/WdfCoInstaller01007.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/usb_driver/amd64/WdfCoInstaller01007.dll -------------------------------------------------------------------------------- /opencv/drive/adb/usb_driver/amd64/WdfCoInstaller01009.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/usb_driver/amd64/WdfCoInstaller01009.dll -------------------------------------------------------------------------------- /opencv/drive/adb/usb_driver/amd64/winusbcoinstaller.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/usb_driver/amd64/winusbcoinstaller.dll -------------------------------------------------------------------------------- /opencv/drive/adb/usb_driver/amd64/winusbcoinstaller2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/usb_driver/amd64/winusbcoinstaller2.dll -------------------------------------------------------------------------------- /opencv/drive/adb/usb_driver/androidwinusb86.cat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/usb_driver/androidwinusb86.cat -------------------------------------------------------------------------------- /opencv/drive/adb/usb_driver/androidwinusba64.cat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/usb_driver/androidwinusba64.cat -------------------------------------------------------------------------------- /opencv/drive/adb/usb_driver/i386/NOTICE.txt: -------------------------------------------------------------------------------- 1 | The .dll files here are distributed by Microsoft Corporation as part of the 2 | Windows Driver Kit (available at 3 | http://www.microsoft.com/whdc/resources/downloads.mspx) and included here as 4 | permitted by the Microsoft Software License Terms. -------------------------------------------------------------------------------- /opencv/drive/adb/usb_driver/i386/WUDFUpdate_01007.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/usb_driver/i386/WUDFUpdate_01007.dll -------------------------------------------------------------------------------- /opencv/drive/adb/usb_driver/i386/WUDFUpdate_01009.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/usb_driver/i386/WUDFUpdate_01009.dll -------------------------------------------------------------------------------- /opencv/drive/adb/usb_driver/i386/WdfCoInstaller01007.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/usb_driver/i386/WdfCoInstaller01007.dll -------------------------------------------------------------------------------- /opencv/drive/adb/usb_driver/i386/WdfCoInstaller01009.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/usb_driver/i386/WdfCoInstaller01009.dll -------------------------------------------------------------------------------- /opencv/drive/adb/usb_driver/i386/winusbcoinstaller.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/usb_driver/i386/winusbcoinstaller.dll -------------------------------------------------------------------------------- /opencv/drive/adb/usb_driver/i386/winusbcoinstaller2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/adb/usb_driver/i386/winusbcoinstaller2.dll -------------------------------------------------------------------------------- /opencv/drive/canny.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | import cv2 3 | import numpy as np 4 | from json import dumps 5 | # 图片路径 6 | IMAGE_NAME = "./img/screen.png" 7 | # 保存为的json文件 8 | JSON_NAME = 'screen.json' 9 | img = cv2.imread(IMAGE_NAME) 10 | 11 | # numpy中ndarray文件转为list 12 | # img_list = img.tolist() 13 | img_list = img[:,:,::-1].tolist() 14 | # print(img_list) 15 | # 字典形式保存数组 16 | img_dict = {} 17 | img_dict['name'] = IMAGE_NAME 18 | img_dict['content'] = img_list 19 | 20 | # 保存为json格式 21 | # json_data = dumps(img_dict, indent=2) 22 | json_data = dumps(img_dict) 23 | 24 | # 将数据保存到文件 25 | with open(JSON_NAME, 'w') as json_file: 26 | json_file.write(json_data) 27 | -------------------------------------------------------------------------------- /opencv/drive/deal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 25 | 26 | 27 | 28 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /opencv/drive/img/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/img/screen.png -------------------------------------------------------------------------------- /opencv/drive/img/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/drive/img/test.jpg -------------------------------------------------------------------------------- /opencv/drive/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 | 28 |
29 |

30 |   31 |

32 |
33 | 34 | 35 | 56 | 57 | -------------------------------------------------------------------------------- /opencv/drive/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drive", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "tyt.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "express": "^4.16.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /opencv/drive/test.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | import cv2 3 | import numpy as np 4 | import json 5 | # 1、图像转换为矩阵 6 | img = cv2.imread('./img/test.jpg') 7 | matrix = np.asarray(img) 8 | 9 | sonStr = json.dumps(matrix, ensure_ascii=False, encoding='UTF-8') 10 | # 将数据保存到文件 11 | #with open('screen.json', 'w') as json_file: 12 | # json_file.write(matrix) 13 | # 2、矩阵转换为图像 14 | # image = Image.fromarray(matrix) -------------------------------------------------------------------------------- /opencv/drive/tyt.js: -------------------------------------------------------------------------------- 1 | //建议选择启动安卓的PTP传输照片模式 2 | //不然可能造成照片读取上传下载出现问题 3 | var express = require('express'); 4 | var child_process = require('child_process'); 5 | //实例化第一个express的应用 6 | var app = express(); 7 | app.get('/screencap', function(req, res) { 8 | res.append("Access-Control-Allow-Origin", "*"); 9 | // 触按屏幕时间 10 | var t; 11 | // 系数5.5 12 | t = req.query.length * 5.5; 13 | new Promise(function(resolve, reject) { 14 | exec(`adb shell input swipe 100 200 100 200 ${parseInt(t)}`, () => { 15 | console.log("跳完了"); 16 | resolve() 17 | }); 18 | }).then(function() { 19 | return new Promise(function(resolve, reject) { 20 | //适当调整延时器是必须的 21 | setTimeout(() => { 22 | screencap(() => { 23 | resolve() 24 | }); 25 | }, 280) 26 | }) 27 | }).then(function() { 28 | res.send('删图成功'); 29 | }) 30 | }); 31 | app.listen(8888); 32 | console.log("开启服务器"); 33 | 34 | function exec(cmd, success, error) { 35 | child_process.exec(cmd, (err, stdout, stderr) => { 36 | if(err) { 37 | console.error(err); 38 | error(); 39 | return; 40 | } 41 | success(); 42 | }); 43 | } 44 | 45 | function screencap(callback) { 46 | //新建文件夹 47 | new Promise(function(resolve, reject) { 48 | //这段可要可不要 49 | exec('adb shell rm -r /sdcard/wscats/', () => { 50 | console.log("删除图片成功"); 51 | resolve(); 52 | }); 53 | }) 54 | .then(function() { 55 | return new Promise(function(resolve, reject) { 56 | exec('adb shell mkdir -p /sdcard/wscats', () => { 57 | console.log("创建文件夹成功"); 58 | resolve(); 59 | }); 60 | }) 61 | }) 62 | .then(function() { 63 | //截图 64 | return new Promise(function(resolve, reject) { 65 | exec('adb shell screencap -p /sdcard/wscats/screen.png', () => { 66 | console.log("截图成功"); 67 | resolve(); 68 | }); 69 | }) 70 | }).then(function() { 71 | //传图 72 | return new Promise(function(resolve, reject) { 73 | exec('adb pull /sdcard/wscats/screen.png ./img', () => { 74 | console.log("上传手机图片成功"); 75 | resolve(); 76 | }, () => { 77 | screencap(callback); 78 | }); 79 | }) 80 | }).then(function() { 81 | //删图 82 | return new Promise(function(resolve, reject) { 83 | exec('adb shell rm -r /sdcard/wscats/', () => { 84 | console.log("删除图片成功"); 85 | resolve(); 86 | }); 87 | }) 88 | }).then(function() { 89 | //转换为矩阵 90 | return new Promise(function(resolve, reject) { 91 | exec('python canny.py', () => { 92 | console.log("处理图片成功"); 93 | callback(); 94 | }); 95 | }) 96 | }) 97 | } -------------------------------------------------------------------------------- /opencv/img/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/opencv/img/test.jpg -------------------------------------------------------------------------------- /opencv/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 | 28 |
29 |

30 |   31 |

32 |
33 | 34 | 35 | 56 | 57 | -------------------------------------------------------------------------------- /opencv/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "undefined": { 6 | "version": "0.1.0", 7 | "resolved": "http://registry.npm.taobao.org/undefined/download/undefined-0.1.0.tgz", 8 | "integrity": "sha1-m3BqSzKtMMIMpP5l3cu72sMr3tA=" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /opencv/test.js: -------------------------------------------------------------------------------- 1 | const cv = require('opencv'); 2 | cv.readImage('./test.jpg', function (err, img) { 3 | if (err) { 4 | throw err; 5 | } 6 | const width = im.width(); 7 | const height = im.height(); 8 | 9 | if (width < 1 || height < 1) { 10 | throw new Error('Image has no size'); 11 | } 12 | // do some cool stuff with img 13 | // save img 14 | img.save('./output.jpg'); 15 | }); -------------------------------------------------------------------------------- /opencv/test.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | import cv2 as cv2 3 | import numpy as np 4 | img = cv2.imread("img/test.jpg") 5 | # cv2.imshow("lena",img) 6 | # cv2.waitKey(10000) 7 | cv2.namedWindow("Image") # 初始化一个名为Image的窗口 8 | cv2.imshow("Image", img) # 显示图片 9 | cv2.waitKey(0) # 等待键盘触发事件,释放窗口 -------------------------------------------------------------------------------- /scrapy/download.py: -------------------------------------------------------------------------------- 1 | import requests 2 | r = requests.get('http://localhost:88/cq1701/python/python-tutorial/scrapy/test.txt') 3 | print(r.content) 4 | with open("download.txt", "wb") as file: 5 | file.write(r.content) -------------------------------------------------------------------------------- /scrapy/download.txt: -------------------------------------------------------------------------------- 1 | 123456 -------------------------------------------------------------------------------- /scrapy/get.py: -------------------------------------------------------------------------------- 1 | import requests 2 | payload = {'name': 'wscats'} 3 | r = requests.get('http://localhost:88/cq1701/python/python-tutorial/scrapy/test.php', params=payload) 4 | print(r.content) -------------------------------------------------------------------------------- /scrapy/post.py: -------------------------------------------------------------------------------- 1 | import requests 2 | payload = { 3 | 'username': 'ly', 4 | 'password': '1234' 5 | } # form-data 6 | 7 | headers = { 8 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1' 9 | } # headers 10 | r = requests.post('http://localhost:88/cs1701/nodejs/day2/login.php', data=payload, headers=headers) 11 | print(r.content) -------------------------------------------------------------------------------- /scrapy/test.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scrapy/test.txt: -------------------------------------------------------------------------------- 1 | 123456 -------------------------------------------------------------------------------- /tutorial/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/.DS_Store -------------------------------------------------------------------------------- /tutorial/install/README.md: -------------------------------------------------------------------------------- 1 | # python 2 | 3 | - [官方安装](https://www.python.org/downloads/) 4 | 5 | # pip 6 | 7 | `easy_install`是`python`默认安装 8 | ```js 9 | sudo easy_install pip 10 | ``` 11 | 查看版本 12 | ```js 13 | pip --version 14 | ``` -------------------------------------------------------------------------------- /tutorial/swiper/.gitignore: -------------------------------------------------------------------------------- 1 | # custom 2 | .idea 3 | medias 4 | .DS_Store 5 | .vscode 6 | .qiniu_* 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | logs 62 | *.log 63 | local_settings.py 64 | *.sqlite3 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # celery beat schedule file 86 | celerybeat-schedule 87 | 88 | # SageMath parsed files 89 | *.sage.py 90 | 91 | # Environments 92 | .env 93 | .venv 94 | env/ 95 | venv/ 96 | ENV/ 97 | env.bak/ 98 | venv.bak/ 99 | 100 | # Spyder project settings 101 | .spyderproject 102 | .spyproject 103 | 104 | # Rope project settings 105 | .ropeproject 106 | 107 | # mkdocs documentation 108 | /site 109 | 110 | # mypy 111 | .mypy_cache/ 112 | -------------------------------------------------------------------------------- /tutorial/swiper/TODO.md: -------------------------------------------------------------------------------- 1 | # Swiper Social 2 | 3 | ## 功能列表 4 | 5 | ### 公共模块 6 | 7 | - [x] Json 接口处理 8 | - [x] 日志处理 9 | - [x] 统一错误处理 10 | - [x] 短信 API 接入 11 | - [x] Celery 异步任务处理 12 | - [x] Redis 底层接口封装 13 | - [x] 缓存处理 14 | 15 | ### 个人模块 16 | 17 | - [x] 用户数据模型设计 18 | - [x] 手机注册 19 | - [x] 短信验证登录 20 | - [x] 获取个人资料 21 | - [x] 修改个人资料 22 | - [x] 头像上传 23 | - 上传图片 24 | - 异步传入七牛云 25 | 26 | ### 交友模块 27 | 28 | - [x] 数据模型设计 29 | - [x] 获取推荐列表 30 | - [x] 匹配检查 31 | - [x] 喜欢 32 | - [x] 超级喜欢 33 | - [x] 不喜欢 34 | - [x] 反悔 35 | - [x] 查看喜欢过我的人 36 | 37 | ### 好友模块 38 | 39 | - [x] 好友表结构设计 40 | - [x] 查看好友列表 41 | - [x] 查看好友信息 42 | 43 | ### VIP 模块 44 | 45 | - [x] 权限数据模型设计 46 | - [x] 权限检查逻辑处理 47 | - [x] 权限详情接口 48 | - [x] 权限: 超级喜欢 / 反悔 / 查看被喜欢 49 | 50 | ### 聊天模块 51 | 52 | - [x] WebSocket 底层接口设计及封装 53 | - [x] Tornado 加载 Django 模块处理 54 | - [x] Tornado 与 Redis 的 Pub / Sub 结合处理消息收发 55 | - [x] 发送消息 56 | - [x] 接收消息 57 | - [x] 拉取离线消息 58 | 59 | ### 运维、部署 60 | 61 | - [x] 部署及维护脚本 62 | - 服务器部署脚本 63 | - 代码上线脚本 64 | - 服务启动、停止、重启 65 | - [x] 异常日志打印 66 | - [x] 日志自动切割 67 | - [x] 异常邮件告警 68 | - [ ] 进程监控 69 | 70 | ## TODO 71 | 72 | ### 前端 73 | 74 | - [x] 登录注册 75 | - [ ] 个人页面 76 | - [x] 滑动页面 77 | - [ ] 陌生人信息页 78 | - [ ] 好友列表页面 79 | - [ ] 聊天页面 80 | 81 | ### 其他 82 | 83 | - [ ] Docker 部署 84 | - [ ] 好友推荐算法 85 | - [ ] 网络资源抓取 86 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/backend/common/__init__.py -------------------------------------------------------------------------------- /tutorial/swiper/backend/common/errors.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | ''' 3 | 程序内部错误 4 | 5 | 程序内部正常的逻辑错误直接抛出异常给前端,经过异常中间件的时候会将对应的错误码返回给前端 6 | ''' 7 | 8 | 9 | class LogicError(Exception): 10 | '''程序内部逻辑错误''' 11 | code = None 12 | data = None 13 | 14 | def __init__(self, data=None): 15 | self.data = data # 发生异常时需要传回前端的数据 16 | 17 | def __str__(self): 18 | return self.__class__.__name__ 19 | 20 | @property 21 | def msg(self): 22 | return self.data or self.__class__.__name__ 23 | 24 | 25 | def gen_error(name: str, err_code: int) -> LogicError: 26 | base_cls = (LogicError,) 27 | cls_attr = {'code': err_code} 28 | return type(name, base_cls, cls_attr) 29 | 30 | 31 | # 正常 32 | OK = gen_error('OK', 0) 33 | 34 | # 通用错误 35 | InternalError = gen_error('InternalError', 500) # 服务器内部错误 36 | ParamsError = gen_error('ParamsError', 1001) # 参数错误 37 | DataError = gen_error('DataError', 1002) # 数据错误 38 | DoseNotExist = gen_error('DoseNotExist', 1003) # 不存在 39 | ReachUpperLimit = gen_error('ReachUpperLimit', 1004) # 达到上限 40 | PermissionDenied = gen_error('PermissionDenied', 1005) # 没有权限 41 | Timeout = gen_error('Timeout', 1006) # 超时 42 | Expired = gen_error('Expired', 1007) # 已过期 43 | NotYetTime = gen_error('NotYetTime', 1008) # 时间未到 44 | InvalidPhone = gen_error('InvalidPhone', 1009) # 无效手机号 45 | InvalidPIN = gen_error('InvalidPIN', 1010) # 无效验证码 46 | 47 | # 用户类错误 48 | LoginRequired = gen_error('LoginRequired', 2000) # 用户未登录 49 | NameConflict = gen_error('NameConflict', 2001) # 名字冲突 50 | MoneyNotEnough = gen_error('MoneyNotEnough', 2002) # 金钱不足 51 | UserNotExist = gen_error('UserNotExist', 2003) # 用户不存在 52 | NotYourFriend = gen_error('NotYourFriend', 2004) # 不是好友关系 53 | 54 | # 第三方错误 55 | WeiboAccessTokenError = gen_error('WeiboAccessTokenError', 9000) # AccessToken 接口错误 56 | WeiboUserShowError = gen_error('WeiboUserShowError', 9000) # UserShow 接口错误 57 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/common/keys.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 各种缓存的 Key 3 | ''' 4 | 5 | LOGIN_SMS_KEY = 'LoginSMS-%s' # phone_num 6 | 7 | MODEL_KEY = 'Model-%s-%s' # model_name, pk 8 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/common/middleware.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | from traceback import format_exc 3 | 4 | from django.http import HttpResponse 5 | from django.utils.deprecation import MiddlewareMixin 6 | 7 | from common import errors 8 | from lib.http import render_json 9 | from lib.mail import async_mail_admins 10 | from user.models import User 11 | 12 | err_log = getLogger('err') 13 | 14 | 15 | class CorsMiddleware(MiddlewareMixin): 16 | '''处理客 JS 户端的跨域''' 17 | def process_request(self, request): 18 | if request.method == 'OPTIONS' and 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' in request.META: 19 | response = HttpResponse() 20 | response['Content-Length'] = '0' 21 | response['Access-Control-Allow-Headers'] = request.META['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] 22 | response['Access-Control-Allow-Origin'] = 'http://127.0.0.1:8000' 23 | return response 24 | 25 | def process_response(self, request, response): 26 | response['Access-Control-Allow-Origin'] = 'http://127.0.0.1:8000' 27 | response['Access-Control-Allow-Credentials'] = 'true' 28 | return response 29 | 30 | 31 | class LogicErrorMiddleware(MiddlewareMixin): 32 | '''通用逻辑异常处理中间件''' 33 | def process_exception(self, request, exception): 34 | if isinstance(exception, errors.LogicError): 35 | response = render_json(error=exception) 36 | else: 37 | error_info = '\n%s' % format_exc() 38 | err_log.error(error_info) # 输出错误日志 39 | async_mail_admins('异常告警', error_info, fail_silently=False) 40 | response = render_json(error=errors.InternalError) 41 | 42 | return response 43 | 44 | 45 | class AuthMiddleware(MiddlewareMixin): 46 | '''登陆认证检查中间件''' 47 | # 不需要检查的路径 48 | IGNORED_PATH_LIST = [ 49 | '/api/user/verify', 50 | '/api/user/login', 51 | '/weibo/' 52 | ] 53 | 54 | def is_ignored_path(self, path): 55 | '''是否是需要忽略的路径''' 56 | for ignored_path in self.IGNORED_PATH_LIST: 57 | if path.startswith(ignored_path): 58 | return True 59 | return False 60 | 61 | def process_request(self, request): 62 | # 排除白名单里的路径 63 | if self.is_ignored_path(request.path): 64 | return 65 | 66 | # 检查 uid 是否存在于 session 中 67 | if 'uid' not in request.session: 68 | return render_json(error=errors.LoginRequired) 69 | 70 | # 为 request 动态添加 user 属性 71 | uid = request.session['uid'] 72 | try: 73 | user = User.get(pk=uid) 74 | request.user = user 75 | except User.DoesNotExist: 76 | return render_json(error=errors.UserNotExist) 77 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/common/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | 4 | PHONENUM_PATTERN = re.compile(r'^1[3-9]\d{9}$') # 预编译手机号匹配规则 5 | 6 | 7 | def is_phonenum(phonenum:str): 8 | '''检查输入的手机号是否正确''' 9 | phonenum = phonenum.strip() 10 | if PHONENUM_PATTERN.match(phonenum): 11 | return True 12 | else: 13 | return False 14 | 15 | 16 | def is_json(test_str): 17 | '''检查字符串是否是 json''' 18 | if not isinstance(test_str, (str, bytes)): 19 | return False 20 | 21 | try: 22 | json.loads(test_str) 23 | except (TypeError, JSONDecodeError): 24 | return False 25 | else: 26 | return True 27 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/backend/lib/__init__.py -------------------------------------------------------------------------------- /tutorial/swiper/backend/lib/cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | from redis import Redis as _Redis 5 | from redis.client import BasePipeline 6 | from pickle import dumps, loads, UnpicklingError 7 | 8 | from django.conf import settings 9 | 10 | 11 | class Redis(_Redis): 12 | ''' 13 | Redis继承类 14 | 15 | 接口与原生 Redis 保持一致,增加自动序列化、反序列化功能 16 | ''' 17 | def __init__(self, *args, **kwargs): 18 | _Redis.__init__(self, *args, **kwargs) 19 | 20 | def keys(self, pattern='*'): 21 | 'Returns a list of keys matching ``pattern``' 22 | return sorted(_Redis.keys(self, pattern)) 23 | 24 | def set(self, key, value, timeout=0): 25 | if timeout > 0: 26 | return self.setex(key, dumps(value, 1), timeout) 27 | else: 28 | return _Redis.set(self, key, dumps(value, 1)) 29 | 30 | def setnx(self, key, value, timeout=0): 31 | res = _Redis.setnx(self, key, dumps(value, 1)) 32 | if res and timeout > 0: 33 | _Redis.expire(self, key, timeout) 34 | return res 35 | 36 | def get(self, key, default=None): 37 | value = _Redis.get(self, key) 38 | return default if value is None else value 39 | 40 | def mset(self, mapping): 41 | return _Redis.mset(self, {k: dumps(v, 1) for k, v in mapping.items()}) 42 | 43 | def mget(self, keys, default=None): 44 | values = _Redis.mget(self, keys) 45 | return [default if v is None else v for v in values] 46 | 47 | def hset(self, name, key, value): 48 | return _Redis.hset(self, name, key, dumps(value, 1)) 49 | 50 | def hget(self, name, key, default=None): 51 | value = _Redis.hget(self, name, key) 52 | return default if value is None else value 53 | 54 | def hmset(self, name, mapping): 55 | return _Redis.hmset(self, name, {k: dumps(v, 1) for k, v in mapping.items()}) 56 | 57 | def hmget(self, name, keys, default=None): 58 | values = _Redis.hmget(self, name, keys) 59 | return [default if v is None else v for v in values] 60 | 61 | def pop(self, key, default=None): 62 | '''del specified key and return the corresponding value''' 63 | pipe = self.pipeline() 64 | pipe.get(key) 65 | pipe.delete(key) 66 | value, res = pipe.execute() 67 | return default if value is None or res != 1 else value 68 | 69 | def hpop(self, name, key, default=None): 70 | '''del specified key and return the value of key within the hash name''' 71 | pipe = self.pipeline() 72 | pipe.hget(name, key) 73 | pipe.hdel(name, key) 74 | value, res = pipe.execute() 75 | return default if value is None or res != 1 else value 76 | 77 | def hscan_iter(self, name, match=None, count=None): 78 | cursor = '0' 79 | found = [] 80 | while cursor != 0: 81 | cursor, data = self.hscan(name, cursor=cursor, 82 | match=match, count=count) 83 | for k, v in data.items(): 84 | if k not in found: 85 | found.append(k) 86 | yield k, v 87 | 88 | def unpickle(self, data): 89 | try: 90 | if isinstance(data, bytes): 91 | return loads(data) 92 | elif isinstance(data, (list, tuple)): 93 | return [self.unpickle(v) for v in data] 94 | elif isinstance(data, dict): 95 | return {k: self.unpickle(v) for k, v in data.items()} 96 | else: 97 | return data 98 | except (UnpicklingError, TypeError, ValueError, EOFError): 99 | return data 100 | 101 | def parse_response(self, connection, command_name, **options): 102 | '''Parses a response from the Redis server''' 103 | response = _Redis.parse_response(self, connection, command_name, **options) 104 | return self.unpickle(response) 105 | 106 | def pipeline(self, transaction=True, shard_hint=None, origin=False): 107 | if origin: 108 | return _Redis.pipeline(self, transaction, shard_hint) 109 | else: 110 | return Pipeline(self.connection_pool, self.response_callbacks, 111 | transaction, shard_hint) 112 | 113 | 114 | class Pipeline(BasePipeline, Redis): 115 | '''覆盖原生Pipeline类''' 116 | def execute(self, raise_on_error=True): 117 | result = super(Pipeline, self).execute(raise_on_error) 118 | return [self.unpickle(r) for r in result] 119 | 120 | 121 | class MSRedis(object): 122 | '''读写分离客户端 (只针对程序中用到的命令)''' 123 | def __init__(self, conf): 124 | self.master = Redis(**conf['Master']) 125 | self.slave = Redis(**conf['Slave']) 126 | self.read_commands = [ 127 | 'ttl', 'exists', 'expire', 'get', 'keys', 128 | 'hget', 'hgetall', 'hkeys', 'hmget', 129 | 'sismember', 'smembers', 'sdiff', 'sinter', 'sunion' 130 | 'zrevrange', 'zrevrangebyscore', 'zrevrank', 'zscore' 131 | ] 132 | 133 | def __getattribute__(self, name): 134 | if name in ['master', 'slave', 'read_commands']: 135 | return object.__getattribute__(self, name) 136 | elif name in self.read_commands: 137 | return self.slave.__getattribute__(name) 138 | else: 139 | return self.master.__getattribute__(name) 140 | 141 | 142 | # 创建全局 Redis 连接 143 | rds = MSRedis(settings.REDIS) 144 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/lib/db.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from lib.cache import rds 4 | from common.keys import MODEL_KEY 5 | 6 | 7 | def get(cls, *args, **kwargs): 8 | '''数据优先从缓存获取, 缓存取不到再从数据库获取''' 9 | # 创建 key 10 | pk = kwargs.get('pk') or kwargs.get('id') 11 | 12 | # 从缓存获取 13 | if pk is not None: 14 | key = MODEL_KEY % (cls.__name__, pk) 15 | model_obj = rds.get(key) 16 | if isinstance(model_obj, cls): 17 | return model_obj 18 | 19 | # 缓存里没有,直接从数据库获取,同时写入缓存 20 | model_obj = cls.objects.get(*args, **kwargs) 21 | key = MODEL_KEY % (cls.__name__, model_obj.pk) 22 | rds.set(key, model_obj) 23 | return model_obj 24 | 25 | 26 | def get_or_create(cls, *args, **kwargs): 27 | # 创建 key 28 | pk = kwargs.get('pk') or kwargs.get('id') 29 | 30 | # 从缓存获取 31 | if pk is not None: 32 | key = MODEL_KEY % (cls.__name__, pk) 33 | model_obj = rds.get(key) 34 | if isinstance(model_obj, cls): 35 | return model_obj, False 36 | 37 | # 执行原生方法,并添加缓存 38 | model_obj, created = cls.objects.get_or_create(*args, **kwargs) 39 | key = MODEL_KEY % (cls.__name__, model_obj.pk) 40 | rds.set(key, model_obj) 41 | return model_obj, created 42 | 43 | 44 | def save(self, force_insert=False, force_update=False, using=None, update_fields=None): 45 | '''存入数据库后,同时写入缓存''' 46 | self._ori_save(force_insert, force_update, using, update_fields) 47 | 48 | # 添加缓存 49 | key = MODEL_KEY % (self.__class__.__name__, self.pk) 50 | rds.set(key, self) 51 | 52 | 53 | def to_dict(self, *ignore): 54 | '''获取对象的属性字典''' 55 | attr_dict = {} 56 | for field in self._meta.fields: 57 | if field.name in ignore: 58 | continue 59 | attr_dict[field.name] = getattr(self, field.name) 60 | return attr_dict 61 | 62 | 63 | def patch_model(): 64 | ''' 65 | 动态更新 Model 方法 66 | 67 | Model 在 Django 中是一个特殊的类, 如果通过继承的方式来增加或修改原有方法, Django 会将 68 | 继承的类识别为一个普通的 app.model, 所以只能通过 monkey patch 的方式动态修改 69 | ''' 70 | # 动态添加一个类方法 get 71 | models.Model.get = classmethod(get) 72 | models.Model.get_or_create = classmethod(get_or_create) 73 | 74 | # 修改 save 75 | models.Model._ori_save = models.Model.save 76 | models.Model.save = save 77 | 78 | # 添加 to_dict 79 | models.Model.to_dict = to_dict 80 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/lib/http.py: -------------------------------------------------------------------------------- 1 | from json import loads 2 | from json import dumps 3 | from json import JSONDecodeError 4 | 5 | from common.errors import OK 6 | from common.errors import LogicError 7 | from django.conf import settings 8 | from django.http import HttpResponse 9 | from django.http import HttpResponseNotAllowed 10 | 11 | 12 | def render_json(data=None, error=OK) -> HttpResponse: 13 | ''' 14 | 将返回值渲染为 JSON 数据 15 | 16 | Params: 17 | data: 返回的数据,一般为一个字典类型,确保每个字段的值都可以被序列化 18 | error: 逻辑错误信息,是 LogicError 的子类或实例 19 | ''' 20 | if isinstance(error, type) and issubclass(error, LogicError): 21 | error = error() 22 | 23 | result = { 24 | 'data': data or error.msg, 25 | 'code': error.code # 状态码 26 | } 27 | 28 | if settings.DEBUG: 29 | # Debug 模式时,按规范格式输出 json 30 | json_str = dumps(result, ensure_ascii=False, indent=4, sort_keys=True) 31 | else: 32 | # 正式环境下,将返回数据压缩 33 | json_str = dumps(result, ensure_ascii=False, separators=[',', ':']) 34 | 35 | return HttpResponse(json_str) 36 | 37 | 38 | def allow_http_methods(*methods): 39 | """检查允许的 HTTP 方法""" 40 | def decor(view_func): 41 | def wrap(request, *args, **kwargs): 42 | nonlocal methods 43 | methods = [m.upper() for m in methods] 44 | if request.method not in methods: 45 | return HttpResponseNotAllowed(methods) 46 | return view_func(request, *args, **kwargs) 47 | return wrap 48 | return decor 49 | 50 | 51 | require_post = allow_http_methods('post') 52 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/lib/mail.py: -------------------------------------------------------------------------------- 1 | from django.core.mail import send_mail, send_mass_mail, mail_admins 2 | 3 | from worker import call_by_worker 4 | 5 | 6 | async_send_mail = call_by_worker(send_mail) 7 | async_send_mass_mail = call_by_worker(send_mass_mail) 8 | async_mail_admins = call_by_worker(mail_admins) 9 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/lib/qiniu.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import qiniu 4 | 5 | from swiper import platform_config as plt 6 | from worker import call_by_worker 7 | 8 | authorization = qiniu.Auth(plt.QN_ACCESS_KEY, plt.QN_SECRET_KEY) 9 | 10 | 11 | def qiniu_upload(bucket_name, filepath, filename=None): 12 | ''' 13 | 向七牛云上传文件 14 | 15 | Args: 16 | bucket_name: 空间名 17 | filepath: 本地文件路径 18 | filename: 上传后的文件名 19 | ''' 20 | if filename is None: 21 | filename = os.path.basename(filepath) 22 | token = authorization.upload_token(bucket_name, filename, 3600) # 生成上传 Token 23 | ret, info = qiniu.put_file(token, filename, filepath) 24 | return ret, info 25 | 26 | 27 | def qiniu_upload_data(bucket_name, filedata, filename): 28 | ''' 29 | 向七牛云上传二进制数据流 30 | 31 | Args: 32 | bucket_name: 空间名 33 | filedata: 二进制数据流 34 | filename: 上传后的文件名 35 | ''' 36 | token = authorization.upload_token(bucket_name, filename, 3600) # 生成上传 Token 37 | ret, info = qiniu.put_data(token, filename, filedata) 38 | return ret, info 39 | 40 | 41 | def qiniu_fetch(bucket_name, resource_url, filename=None): 42 | ''' 43 | 由七牛抓取网络资源到空间 44 | 45 | Args: 46 | bucket_name: 空间名 47 | resource_url: 网络资源地址 48 | filename: 上传后的文件名 49 | ''' 50 | bucket = qiniu.BucketManager(authorization) 51 | ret, info = bucket.fetch(resource_url, bucket_name, filename) 52 | return ret, info 53 | 54 | 55 | async_qiniu_upload = call_by_worker(qiniu_upload) 56 | async_qiniu_upload_data = call_by_worker(qiniu_upload_data) 57 | async_qiniu_fetch = call_by_worker(qiniu_fetch) 58 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/lib/sms.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import requests 4 | 5 | from swiper import platform_config 6 | from worker import call_by_worker 7 | 8 | 9 | def gen_verify_code(length=4): 10 | '''生成验证码''' 11 | if length <= 0: 12 | length = 1 13 | code = random.randrange(10 ** (length - 1), 10 ** (length)) 14 | return str(code) 15 | 16 | 17 | def send_sms(phone_num, text): 18 | '''发送短信''' 19 | params = platform_config.HY_SMS_PARAMS.copy() 20 | params['mobile'] = phone_num 21 | params['content'] = params['content'] % text 22 | headers = { 23 | "Accept": "text/plain", 24 | "Content-type": "application/x-www-form-urlencoded" 25 | } 26 | response = requests.post(platform_config.HY_SMS_URL, data=params, headers=headers) 27 | return response 28 | 29 | 30 | async_send_sms = call_by_worker(send_sms) # 为方便调试,将异步调用单独定义一次 31 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "swiper.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/social/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/backend/social/__init__.py -------------------------------------------------------------------------------- /tutorial/swiper/backend/social/api.py: -------------------------------------------------------------------------------- 1 | from lib.http import require_post 2 | from lib.http import render_json 3 | from vip.logic import need_perm 4 | from social import logic 5 | from social.models import Friends 6 | 7 | 8 | def recommend(request): 9 | '''获取推荐列表''' 10 | users = logic.get_recommend_users(request.user) 11 | return render_json({'users': [u.to_dict() for u in users]}) 12 | 13 | 14 | @require_post 15 | def like(request): 16 | '''喜欢''' 17 | stranger_id = int(request.POST.get('stranger_id')) 18 | return render_json({'matched': logic.like(stranger_id)}) 19 | 20 | 21 | @require_post 22 | @need_perm('superlike') 23 | def superlike(request): 24 | '''超级喜欢''' 25 | stranger_id = int(request.POST.get('stranger_id')) 26 | return render_json({'matched': logic.superlike(stranger_id)}) 27 | 28 | 29 | @require_post 30 | def dislike(request): 31 | '''不喜欢''' 32 | stranger_id = int(request.POST.get('stranger_id')) 33 | logic.dislike(stranger_id) 34 | return render_json() 35 | 36 | 37 | @require_post 38 | @need_perm('rewind') 39 | def rewind(request): 40 | '''反悔''' 41 | stranger_id = int(request.POST.get('stranger_id')) 42 | return render_json(logic.rewind(stranger_id)) 43 | 44 | 45 | @need_perm('liked_me') 46 | def who_liked_me(request): 47 | '''查看谁喜欢过我''' 48 | users = logic.get_users_liked_me(request.user) 49 | return render_json({'users': users}) 50 | 51 | 52 | def friend_list(request): 53 | '''好友列表''' 54 | user = request.user 55 | friends = [f.to_dict() for f in user.friends()] 56 | return render_json({'friends': friends}) 57 | 58 | 59 | @require_post 60 | def break_off(request): 61 | '''与对方绝交''' 62 | stranger_id = int(request.POST.get('stranger_id')) 63 | Friends.break_off(request.session.uid, stranger_id) 64 | return render_json() 65 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/social/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SocialConfig(AppConfig): 5 | name = 'social' 6 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/social/logic.py: -------------------------------------------------------------------------------- 1 | from social.models import Swiped 2 | from social.models import Friends 3 | 4 | from user.models import User 5 | 6 | 7 | def get_recommend_users(user): 8 | ''' 9 | 获取推荐列表 10 | 11 | TODO: 当前算法仅仅是随机筛选做的伪实现, 后期需要修改 12 | ''' 13 | import random 14 | total = User.objects.count() 15 | start = random.randrange(total - 30) 16 | end = start + 30 17 | return User.objects.all()[start:end] 18 | 19 | 20 | def like(user, stranger_id): 21 | '''喜欢''' 22 | Swiped.swipe_right(user.id, stranger_id) 23 | 24 | # 检查对方是否喜欢过自己 25 | if Swiped.is_liked(stranger_id, user.id): 26 | Friends.be_friends(user.id, stranger_id) 27 | # TODO: 向添加好友的双方实时推送消息 28 | return True 29 | else: 30 | return False 31 | 32 | 33 | def superlike(user, stranger_id): 34 | '''超级喜欢''' 35 | Swiped.swipe_up(user.id, stranger_id) 36 | 37 | # 检查对方是否喜欢过自己 38 | if Swiped.is_liked(stranger_id, user.id): 39 | Friends.be_friends(user.id, stranger_id) 40 | # TODO: 向添加好友的双方实时推送消息 41 | return True 42 | else: 43 | return False 44 | 45 | 46 | def dislike(user, stranger_id): 47 | '''不喜欢''' 48 | Swiped.swipe_left(user.id, stranger_id) 49 | 50 | 51 | def rewind(user, stranger_id): 52 | '''反悔''' 53 | try: 54 | Swiped.objects.get(uid=user.id, sid=stranger_id).delete() 55 | except Swiped.DoesNotExist: 56 | pass 57 | 58 | 59 | def get_users_liked_me(user): 60 | '''查看喜欢过过我的用户''' 61 | return Swiped.liked_me(user.id) 62 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/social/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.15 on 2018-10-06 07:56 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Friends', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('uid1', models.IntegerField()), 21 | ('uid2', models.IntegerField()), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='Swiped', 26 | fields=[ 27 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('uid', models.IntegerField(db_index=True, verbose_name='用户自身 id')), 29 | ('sid', models.IntegerField(db_index=True, verbose_name='被滑的陌生人 id')), 30 | ('mark', models.CharField(choices=[('like', '喜欢'), ('superlike', '喜欢'), ('dislike', '喜欢')], db_index=True, max_length=16, verbose_name='滑动类型')), 31 | ('time', models.DateTimeField(auto_now_add=True, verbose_name='滑动的时间')), 32 | ], 33 | options={ 34 | 'ordering': ['-time', 'uid', 'sid'], 35 | }, 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/social/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/backend/social/migrations/__init__.py -------------------------------------------------------------------------------- /tutorial/swiper/backend/social/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models import Q 3 | 4 | 5 | class Swiped(models.Model): 6 | '''滑过的记录''' 7 | MARK = ( 8 | ('like', '喜欢'), 9 | ('superlike', '超级喜欢'), 10 | ('dislike', '不喜欢'), 11 | ) 12 | 13 | uid = models.IntegerField(db_index=True, verbose_name='用户自身 id') 14 | sid = models.IntegerField(db_index=True, verbose_name='被滑的陌生人 id') 15 | mark = models.CharField(max_length=16, db_index=True, choices=MARK, verbose_name='滑动类型') 16 | time = models.DateTimeField(auto_now_add=True, verbose_name='滑动的时间') 17 | 18 | class Meta: 19 | ordering = ['-time', 'uid', 'sid'] 20 | 21 | @classmethod 22 | def is_liked(cls, uid, sid): 23 | condition = Q(mark='like') | Q(mark='superlike') 24 | if cls.objects.filter(condition, uid=uid, sid=sid).exists(): 25 | return True 26 | return False 27 | 28 | @classmethod 29 | def swipe_right(cls, uid, sid): 30 | '''右滑''' 31 | defaults = {'mark': 'like'} 32 | cls.objects.update_or_create(uid=user.id, sid=stranger_id, defaults=defaults) 33 | 34 | @classmethod 35 | def swipe_up(cls, uid, sid): 36 | '''上滑''' 37 | defaults = {'mark': 'superlike'} 38 | cls.objects.update_or_create(uid=user.id, sid=stranger_id, defaults=defaults) 39 | 40 | @classmethod 41 | def swipe_left(cls, uid, sid): 42 | '''左滑''' 43 | defaults = {'mark': 'dislike'} 44 | cls.objects.update_or_create(uid=user.id, sid=stranger_id, defaults=defaults) 45 | 46 | @classmethod 47 | def liked(cls, uid): 48 | '''我喜欢过的''' 49 | condition = Q(mark='like') | Q(mark='superlike') 50 | return cls.objects.filter(condition, uid=uid) 51 | 52 | @classmethod 53 | def liked_me(cls, uid): 54 | '''喜欢我的''' 55 | condition = Q(mark='like') | Q(mark='superlike') 56 | return cls.objects.filter(condition, sid=uid) 57 | 58 | 59 | class Friends(models.Model): 60 | ''' 61 | 好友关系表 62 | 63 | User 表自身的“多对多”关系, 有两个 uid 字段。 64 | 为了数据量更精简,用户 A 与 用户 B 是好友关系只会产生一条记录,取其中较小的做 uid1, 较大的做 uid2 65 | ''' 66 | uid1 = models.IntegerField() 67 | uid2 = models.IntegerField() 68 | 69 | @classmethod 70 | def friend_id_list(cls, uid): 71 | condition = Q(uid1=uid) | Q(uid2=uid) 72 | relstions = cls.objects.filter(condition) 73 | fid_list = [] 74 | for r in relstions: 75 | friend_id = r.uid2 if uid == r.uid1 else r.uid1 76 | fid_list.append(friend_id) 77 | return fid_list 78 | 79 | @classmethod 80 | def is_friends(cls, uid1, uid2): 81 | '''检查是否是朋友关系''' 82 | uid1, uid2 = sorted([uid1, uid2]) 83 | return cls.objects.filter(uid1=uid1, uid2=uid2).exists() 84 | 85 | @classmethod 86 | def be_friends(cls, uid1, uid2): 87 | '''建立好友关系''' 88 | uid1, uid2 = sorted([uid1, uid2]) 89 | cls.objects.get_or_create(uid1=uid1, uid2=uid2) 90 | 91 | @classmethod 92 | def break_off(cls, uid1, uid2): 93 | '''断绝好友关系''' 94 | uid1, uid2 = sorted([uid1, uid2]) 95 | try: 96 | cls.objects.get(uid1=uid1, uid2=uid2).delete() 97 | except cls.DoesNotExists: 98 | pass 99 | 100 | condition = Q(uid=uid1, sid=uid2) | Q(uid=uid2, sid=uid1) 101 | Swiped.objects.filter(condition).update(mark='dislike') 102 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/swiper/__init__.py: -------------------------------------------------------------------------------- 1 | from lib.db import patch_model 2 | 3 | patch_model() # monkey patch 4 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/swiper/gunicorn_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | from multiprocessing import cpu_count 5 | 6 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 7 | 8 | bind = ["127.0.0.1:9000"] # 线上环境不会开启在公网 IP 下,一般使用内网 IP 9 | daemon = True # 是否开启守护进程模式 10 | pidfile = '{BASE_DIR}/logs/gunicorn.pid' 11 | 12 | workers = cpu_count() * 2 13 | worker_class = "gevent" # 指定一个异步处理的库 14 | forwarded_allow_ips = '*' 15 | 16 | keepalive = 60 17 | timeout = 30 18 | graceful_timeout = 10 19 | worker_connections = 65535 20 | 21 | # 日志处理 22 | capture_output = True 23 | loglevel = 'info' 24 | errorlog = '{BASE_DIR}/logs/error.log' 25 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/swiper/platform_config.py: -------------------------------------------------------------------------------- 1 | '''各个第三方平台的接入配置''' 2 | 3 | # 互亿无限短信配置 4 | HY_SMS_URL = 'http://106.ihuyi.com/webservice/sms.php?method=Submit' 5 | HY_SMS_PARAMS = { 6 | 'account': 'C42331298', 7 | 'password': '2d2284b74dc4972da3df3915fb17b28f', 8 | 'content': '您的验证码是:%s。请不要把验证码泄露给其他人。', 9 | 'mobile': None, 10 | 'format': 'json' 11 | } 12 | 13 | 14 | # 七牛云账号配置 15 | QN_ACCESS_KEY = 'kEM0sRR-meB92XU43_a6xZqhiyyTuu5yreGCbFtw' 16 | QN_SECRET_KEY = 'QxTKqgnOb_UVldphU261qu9IdzmjkgGHh6GQVPPy' 17 | QN_BASE_URL = 'http://ph3wmx4s2.bkt.clouddn.com' 18 | QN_BUCKET = 'swiper' 19 | 20 | 21 | # 微博 OAuth 认证配置 22 | WB_APP_KEY = '415847342' 23 | WB_APP_SECRET = '25bb6f5efd2f2d69177095562f031e3b' 24 | WB_CALLBACK = 'http://swiper.seamile.org/weibo/callback/' 25 | 26 | # 微博授权认证接口 27 | WB_AUTH_API = 'https://api.weibo.com/oauth2/authorize' 28 | WB_AUTH_ARGS = { 29 | 'client_id': WB_APP_KEY, 30 | 'redirect_uri': WB_CALLBACK, 31 | } 32 | 33 | # 获取微博令牌接口 34 | WB_ACCESS_TOKEN_API = 'https://api.weibo.com/oauth2/access_token' 35 | WB_ACCESS_TOKEN_ARGS = { 36 | 'client_id': WB_APP_KEY, 37 | 'client_secret': WB_APP_SECRET, 38 | 'grant_type': 'authorization_code', 39 | 'redirect_uri': WB_CALLBACK, 40 | 'code': None, 41 | } 42 | 43 | # 获取微博用户数据接口 44 | WB_USER_SHOW_API = 'https://api.weibo.com/2/users/show.json' 45 | WB_USER_SHOW_ARGS = { 46 | 'access_token': None, 47 | 'uid': None, 48 | } 49 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/swiper/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for swiper project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'k$gybxt1($!)w=0=(7+@-f(wz&9t*z7joo41jike@3me6wm!nx' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.staticfiles', 39 | 'corsheaders', 40 | 'user', 41 | 'social', 42 | 'vip', 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'common.middleware.CorsMiddleware', 49 | 'django.middleware.common.CommonMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | 'common.middleware.LogicErrorMiddleware', 52 | 'common.middleware.AuthMiddleware', 53 | ] 54 | 55 | ROOT_URLCONF = 'swiper.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.template.context_processors.debug', 66 | 'django.template.context_processors.request', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'swiper.wsgi.application' 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 77 | 78 | DATABASES = { 79 | 'default': { 80 | 'ENGINE': 'django.db.backends.sqlite3', 81 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 82 | } 83 | } 84 | 85 | 86 | # Password validation 87 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 92 | }, 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 101 | }, 102 | ] 103 | 104 | 105 | # Internationalization 106 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 107 | 108 | LANGUAGE_CODE = 'zh-hans' 109 | 110 | TIME_ZONE = 'Asia/Shanghai' 111 | 112 | USE_I18N = True 113 | 114 | USE_L10N = True 115 | 116 | USE_TZ = True 117 | 118 | 119 | # Static files (CSS, JavaScript, Images) 120 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 121 | 122 | STATIC_URL = '/static/' 123 | 124 | 125 | # Redis 126 | REDIS = { 127 | 'Master': { 128 | 'host': 'localhost', 129 | 'port': 6379, 130 | 'db': 15 131 | }, 132 | 'Slave': { 133 | 'host': 'localhost', 134 | 'port': 6379, 135 | 'db': 15 136 | }, 137 | } 138 | 139 | 140 | # Email 配置 141 | ADMINS = [ 142 | ('John', 'john@example.com'), 143 | ('Mary', 'mary@example.com') 144 | ] 145 | EMAIL_SUBJECT_PREFIX = '[Swiper] ' 146 | 147 | 148 | # Logging 149 | LOGGING = { 150 | 'version': 1, 151 | 'disable_existing_loggers': True, 152 | 'formatters': { 153 | 'simple': { 154 | 'format': '%(asctime)s %(module)s.%(funcName)s: %(message)s', 155 | 'datefmt': '%Y-%m-%d %H:%M:%S', 156 | }, 157 | 'verbose': { 158 | 'format': ('%(asctime)s %(levelname)s [%(process)d-%(threadName)s] ' 159 | '%(module)s.%(funcName)s line %(lineno)d: %(message)s'), 160 | 'datefmt': '%Y-%m-%d %H:%M:%S', 161 | } 162 | }, 163 | 164 | 'handlers': { 165 | 'console': { 166 | 'class': 'logging.StreamHandler', 167 | 'level': 'DEBUG' 168 | }, 169 | 'info': { 170 | 'class': 'logging.handlers.TimedRotatingFileHandler', 171 | 'filename': f'{BASE_DIR}/logs/info.log', 172 | 'when': 'D', # 每天切割日志 173 | 'backupCount': 30, # 日志保留 30 天 174 | 'formatter': 'simple', 175 | 'level': 'DEBUG' 176 | }, 177 | 'error': { 178 | 'class': 'logging.handlers.TimedRotatingFileHandler', 179 | 'filename': f'{BASE_DIR}/logs/error.log', 180 | 'when': 'W0', # 每周一切割日志 181 | 'backupCount': 4, # 日志保留 4 周 182 | 'formatter': 'verbose', 183 | 'level': 'DEBUG' 184 | } 185 | }, 186 | 187 | 'loggers': { 188 | 'django': { 189 | 'handlers': ['console'], 190 | # 'level': 'DEBUG' if DEBUG else 'INFO', 191 | }, 192 | 'inf': { 193 | 'handlers': ['info'], 194 | 'propagate': True, 195 | 'level': 'DEBUG' if DEBUG else 'INFO', 196 | }, 197 | 'err': { 198 | 'handlers': ['error'], 199 | 'propagate': True, 200 | 'level': 'DEBUG' if DEBUG else 'ERROR', 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/swiper/urls.py: -------------------------------------------------------------------------------- 1 | """swiper URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url 17 | 18 | from user import api as user_api 19 | from social import api as social_api 20 | from vip import api as vip_api 21 | 22 | 23 | urlpatterns = [ 24 | # User API 25 | url(r'^api/user/verify$', user_api.verify_phone), 26 | url(r'^api/user/login$', user_api.login), 27 | url(r'^api/user/profile/show$', user_api.show_profile), 28 | url(r'^api/user/profile/update$', user_api.update_profile), 29 | url(r'^api/user/avatar/upload$', user_api.upload_avatar), 30 | # WeiBo 31 | url(r'weibo/authurl$', user_api.weibo_authurl), 32 | url(r'weibo/callback$', user_api.weibo_callback), 33 | 34 | # Social API 35 | url(r'api/social/recommend$', social_api.recommend), 36 | url(r'api/social/like$', social_api.like), 37 | url(r'api/social/superlike$', social_api.superlike), 38 | url(r'api/social/dislike$', social_api.dislike), 39 | url(r'api/social/rewind$', social_api.rewind), 40 | url(r'api/social/likedme$', social_api.who_liked_me), 41 | url(r'api/social/friends$', social_api.friend_list), 42 | url(r'api/social/break_off$', social_api.break_off), 43 | 44 | # VIP API 45 | url(r'api/vip/info$', vip_api.vip_info), 46 | ] 47 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/swiper/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for swiper project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "swiper.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/backend/user/__init__.py -------------------------------------------------------------------------------- /tutorial/swiper/backend/user/api.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlencode 2 | 3 | from swiper import platform_config 4 | from lib.cache import rds 5 | from lib.http import require_post, render_json 6 | from common import errors 7 | from common import keys 8 | from common.utils import is_phonenum 9 | from user.models import User 10 | from user.forms import ProfileForm 11 | from user.logic import send_login_code 12 | from user.logic import upload_avatar_to_cloud 13 | from user.logic import get_wb_access_token 14 | from user.logic import wb_user_show 15 | 16 | 17 | def verify_phone(request): 18 | '''提交手机号,向用户发送验证码''' 19 | phone_num = request.GET.get('phone', '') 20 | if is_phonenum(phone_num): 21 | send_login_code(phone_num) 22 | return render_json() 23 | else: 24 | raise errors.InvalidPhone 25 | 26 | 27 | @require_post 28 | def login(request): 29 | '''提交验证码并登录''' 30 | phone_num = request.POST.get('phone') 31 | code = request.POST.get('code') 32 | key = keys.LOGIN_SMS_KEY % phone_num 33 | if rds.get(key) != code: 34 | raise errors.InvalidPIN 35 | 36 | # 获取用户,并执行登陆操作 37 | user, created = User.get_or_create(phonenum=phone_num) 38 | if created: 39 | user.init() 40 | request.session['uid'] = user.id 41 | return render_json({'user': user.to_dict()}) 42 | 43 | 44 | def show_profile(request): 45 | '''查看配置''' 46 | result = request.user.profile.to_dict() 47 | return render_json(result) 48 | 49 | 50 | @require_post 51 | def update_profile(request): 52 | '''修改用户配置''' 53 | profile = request.user.profile 54 | form = ProfileForm(request.POST, instance=profile) 55 | if form.is_valid(): 56 | form.save() 57 | return render_json() 58 | else: 59 | raise errors.ParamsError 60 | 61 | 62 | @require_post 63 | def upload_avatar(request): 64 | '''上传头像''' 65 | avatar = request.user.avatar 66 | upload_avatar_to_cloud(avatar, request.POST) 67 | return render_json() 68 | 69 | 70 | def weibo_authurl(request): 71 | auth_url = '%s?%s' % (platform_config.WB_AUTH_API, urlencode(platform_config.WB_AUTH_ARGS)) 72 | return render_json({'auth_url': auth_url}) 73 | 74 | 75 | def weibo_callback(request): 76 | '''微博回调接口''' 77 | code = request.GET.get('code') 78 | 79 | # 获取 access_token 80 | access_token, wb_uid = get_wb_access_token(code) 81 | if access_token is None: 82 | raise errors.WeiboAccessTokenError 83 | 84 | # 获取微博的用户数据 85 | screen_name, avatar = wb_user_show(access_token, wb_uid) 86 | if screen_name is None: 87 | raise errors.WeiboUserShowError 88 | 89 | # 利用微博的账号,在论坛内进行登陆、注册 90 | nickname = '%s_wb' % screen_name 91 | user, is_created = User.get_or_create(nickname=nickname) 92 | user.avatar.first = avatar 93 | user.avatar.save() 94 | user.save() 95 | 96 | # 记录用户状态 97 | request.session['uid'] = user.id 98 | 99 | return render_json({'user': user.to_dict()}) 100 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/user/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UserConfig(AppConfig): 5 | name = 'user' 6 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/user/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from user.models import Profile 4 | 5 | 6 | class ProfileForm(forms.ModelForm): 7 | class Meta: 8 | model = Profile 9 | fields = ['location', 'min_distance', 'max_distance', 'min_dating_age', 10 | 'max_dating_age', 'dating_sex', 'vibration', 'only_matche', 11 | 'auto_play'] 12 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/user/logic.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from lib.cache import rds 4 | from lib import sms 5 | from lib.qiniu import qiniu_upload_data 6 | from common import keys 7 | from common import errors 8 | from swiper import platform_config 9 | from worker import call_by_worker 10 | 11 | 12 | def send_login_code(phone_num): 13 | '''发送登陆验证短信''' 14 | key = keys.LOGIN_SMS_KEY % phone_num 15 | if not rds.exists(key): 16 | random_code = sms.gen_verify_code(4) 17 | sms.async_send_sms(phone_num, random_code) 18 | rds.setex(key, random_code, 180) # 状态码有效期 180s 19 | else: 20 | raise errors.NotYetTime 21 | 22 | 23 | @call_by_worker 24 | def upload_avatar_to_cloud(avatar, files): 25 | '''将图片上传至七牛云''' 26 | for field_name, file_obj in files.items(): 27 | # 上传 28 | filename = 'avatar-%s-%s' % (avatar.id, field_name) 29 | qiniu_upload_data(platform_config.QN_BUCKET, file_obj, filename) 30 | # 设置属性 31 | url = '%s/%s' % (platform_config.QN_BASE_URL, filename) 32 | setattr(avatar, field_name, url) 33 | avatar.save() 34 | 35 | 36 | def get_wb_access_token(code): 37 | '''获取微博的 Access Token''' 38 | # 构造参数 39 | args = platform_config.WB_ACCESS_TOKEN_ARGS.copy() 40 | args['code'] = code 41 | 42 | response = requests.post(platform_config.WB_ACCESS_TOKEN_API, data=args) # 发送请求 43 | data = response.json() # 提取数据 44 | if 'access_token' in data: 45 | access_token = data['access_token'] 46 | uid = data['uid'] 47 | return access_token, uid 48 | else: 49 | return None, None 50 | 51 | 52 | def wb_user_show(access_token, wb_uid): 53 | '''根据微博用户ID获取用户信息''' 54 | # 构造参数 55 | args = platform_config.WB_USER_SHOW_ARGS 56 | args['access_token'] = access_token 57 | args['uid'] = wb_uid 58 | 59 | # 发送请求 60 | response = requests.get(platform_config.WB_USER_SHOW_API, params=args) 61 | data = response.json() 62 | if 'screen_name' in data: 63 | screen_name = data['screen_name'] 64 | avatar = data['avatar_hd'] 65 | return screen_name, avatar 66 | else: 67 | return None, None 68 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/user/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.15 on 2018-10-06 07:56 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Avatar', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('first', models.URLField(blank=True, null=True)), 21 | ('second', models.URLField(blank=True, null=True)), 22 | ('third', models.URLField(blank=True, null=True)), 23 | ('fourth', models.URLField(blank=True, null=True)), 24 | ('fifth', models.URLField(blank=True, null=True)), 25 | ('sixth', models.URLField(blank=True, null=True)), 26 | ], 27 | ), 28 | migrations.CreateModel( 29 | name='Profile', 30 | fields=[ 31 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('location', models.CharField(max_length=32, verbose_name='目标城市')), 33 | ('min_distance', models.FloatField(default=1.0, verbose_name='最小查找范围')), 34 | ('max_distance', models.FloatField(default=50.0, verbose_name='最大查找范围')), 35 | ('min_dating_age', models.IntegerField(default=18, verbose_name='最小交友年龄')), 36 | ('max_dating_age', models.IntegerField(default=50, verbose_name='最大交友年龄')), 37 | ('dating_sex', models.CharField(choices=[('Male', '男性'), ('Female', '女性'), ('All', '不限')], max_length=16, verbose_name='匹配的性别')), 38 | ('vibration', models.BooleanField(default=True, verbose_name='开启震动')), 39 | ('only_matche', models.BooleanField(default=False, verbose_name='不让为匹配的人看我的相册')), 40 | ('auto_play', models.BooleanField(default=False, verbose_name='自动播放视频')), 41 | ], 42 | ), 43 | migrations.CreateModel( 44 | name='User', 45 | fields=[ 46 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 47 | ('phonenum', models.CharField(max_length=16, unique=True)), 48 | ('nickname', models.CharField(max_length=16)), 49 | ('sex', models.CharField(choices=[('Male', '男'), ('Female', '女')], max_length=16)), 50 | ('birth_year', models.IntegerField(default=2000)), 51 | ('birth_month', models.IntegerField(default=1)), 52 | ('birth_day', models.IntegerField(default=1)), 53 | ('location', models.CharField(max_length=32, verbose_name='常居地')), 54 | ('vip_id', models.IntegerField(default=1)), 55 | ('vip_expiration', models.DateTimeField(auto_now_add=True, verbose_name='会员过期时间')), 56 | ], 57 | ), 58 | ] 59 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/user/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/backend/user/migrations/__init__.py -------------------------------------------------------------------------------- /tutorial/swiper/backend/user/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.db import models 4 | from django.utils.functional import cached_property 5 | 6 | from vip.models import Vip 7 | from social.models import Friends 8 | 9 | 10 | class User(models.Model): 11 | SEX = ( 12 | ('Male', '男'), 13 | ('Female', '女'), 14 | ) 15 | 16 | phonenum = models.CharField(max_length=16, unique=True) 17 | nickname = models.CharField(max_length=16) 18 | 19 | # user info 20 | sex = models.CharField(max_length=16, choices=SEX, blank=False, null=False) 21 | birth_year = models.IntegerField(default=2000) 22 | birth_month = models.IntegerField(default=1) 23 | birth_day = models.IntegerField(default=1) 24 | location = models.CharField(max_length=32, verbose_name='常居地') 25 | 26 | vip_id = models.IntegerField(default=1) # 关联的 vip id 27 | vip_expiration = models.DateTimeField(auto_now_add=True, verbose_name="会员过期时间") 28 | 29 | def init(self): 30 | '''TODO: 创建新用户的初始化操作''' 31 | pass 32 | 33 | @cached_property 34 | def age(self): 35 | '''年龄''' 36 | birthday = datetime.date(self.birth_year, self.birth_month, self.birth_day) 37 | return (datetime.date.today() - birthday).days // 365 38 | 39 | @cached_property 40 | def avatar(self): 41 | '''头像''' 42 | return Avatar.get_or_create(id=self.id)[0] 43 | 44 | @cached_property 45 | def profile(self): 46 | '''资料''' 47 | return Profile.get_or_create(id=self.id)[0] 48 | 49 | @cached_property 50 | def vip(self): 51 | '''用户会员''' 52 | return Vip.get(id=self.vip_id) 53 | 54 | @cached_property 55 | def friends(self): 56 | '''用户的好友列表''' 57 | fid_list = Friends.friend_id_list(self.id) 58 | return User.objects.filter(id__in=fid_list) # objects 是特殊的类属性, 只能通过类调用 59 | 60 | @cached_property 61 | def is_dating_ready(self): 62 | '''检查资料是否完整''' 63 | pass 64 | 65 | def to_dict(self): 66 | return { 67 | 'uid': self.id, 68 | 'nickname': self.nickname, 69 | 'age': self.age, 70 | 'sex': self.sex, 71 | 'location': self.location, 72 | 'avatars': list(self.avatar), 73 | } 74 | 75 | 76 | class Avatar(models.Model): 77 | ''' 78 | 用户形象 79 | 80 | 与 User 是“一对一”关系,直接与 User 表 id 保持一致 81 | ''' 82 | first = models.URLField(null=True, blank=True) 83 | second = models.URLField(null=True, blank=True) 84 | third = models.URLField(null=True, blank=True) 85 | fourth = models.URLField(null=True, blank=True) 86 | fifth = models.URLField(null=True, blank=True) 87 | sixth = models.URLField(null=True, blank=True) 88 | 89 | def __iter__(self): 90 | urls = [self.first, self.second, self.third, 91 | self.fourth, self.fifth, self.sixth] 92 | return filter(None, urls) # 取出非空头像 93 | 94 | @cached_property 95 | def head(self): 96 | '''选择第一张图片作为头像''' 97 | return self.first 98 | 99 | 100 | class Profile(models.Model): 101 | ''' 102 | 用户个人配置 103 | 104 | 与 User 是“一对一”关系,直接与 User 表 id 保持一致 105 | ''' 106 | SEX = ( 107 | ('Male', '男性'), 108 | ('Female', '女性'), 109 | ('All', '不限'), 110 | ) 111 | 112 | # 交友设置 113 | location = models.CharField(max_length=32, verbose_name='目标城市') 114 | min_distance = models.FloatField(default=1.0, verbose_name='最小查找范围') 115 | max_distance = models.FloatField(default=50.0, verbose_name='最大查找范围') 116 | min_dating_age = models.IntegerField(default=18, verbose_name='最小交友年龄') 117 | max_dating_age = models.IntegerField(default=50, verbose_name='最大交友年龄') 118 | dating_sex = models.CharField(max_length=16, choices=SEX, verbose_name='匹配的性别') 119 | 120 | # 其他设置 121 | vibration = models.BooleanField(default=True, verbose_name='开启震动') 122 | only_matche = models.BooleanField(default=False, verbose_name='不让为匹配的人看我的相册') 123 | auto_play = models.BooleanField(default=False, verbose_name='自动播放视频') 124 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/vip/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/backend/vip/__init__.py -------------------------------------------------------------------------------- /tutorial/swiper/backend/vip/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/vip/api.py: -------------------------------------------------------------------------------- 1 | from vip.models import Vip 2 | 3 | 4 | def vip_info(request): 5 | '''枚举所有 VIP 的权限''' 6 | vips = {} 7 | for vip in Vip.objects.all(): 8 | perms = ((perm.name, perm.description) for perm in vip.perms()) 9 | vips[vip.level] = {'price': vip.price, 'perms': sorted(perms)} 10 | return vips 11 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/vip/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class VipConfig(AppConfig): 5 | name = 'vip' 6 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/vip/logic.py: -------------------------------------------------------------------------------- 1 | from common.errors import PermissionDenied 2 | 3 | 4 | def need_perm(perm_name): 5 | '''Vip 权限检查装饰器''' 6 | def check(view_function): 7 | def wrapper(request): 8 | user = request.user 9 | if user.vip.has_perm(perm_name): 10 | return view_function(request) 11 | else: 12 | raise PermissionDenied 13 | return wrapper 14 | return check 15 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/vip/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.7 on 2018-10-05 16:13 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Permission', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=32)), 21 | ('description', models.TextField(verbose_name='权限详情介绍')), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='Vip', 26 | fields=[ 27 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('name', models.CharField(max_length=16, unique=True)), 29 | ('level', models.IntegerField(unique=True, verbose_name='会员等级')), 30 | ('price', models.FloatField(verbose_name='充值会员的价格, 单位:元')), 31 | ], 32 | options={ 33 | 'ordering': ['level', 'name'], 34 | }, 35 | ), 36 | migrations.CreateModel( 37 | name='VipPermRelation', 38 | fields=[ 39 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 40 | ('vip_id', models.IntegerField()), 41 | ('perm_id', models.IntegerField()), 42 | ], 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/vip/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/backend/vip/migrations/__init__.py -------------------------------------------------------------------------------- /tutorial/swiper/backend/vip/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Vip(models.Model): 5 | name = models.CharField(max_length=16, unique=True) 6 | level = models.IntegerField(unique=True, verbose_name='会员等级') 7 | price = models.FloatField(verbose_name='充值会员的价格, 单位:元') 8 | 9 | class Meta: 10 | ordering = ['level', 'name'] 11 | 12 | def perms(self): 13 | relations = VipPermRelation.objects.filter(vip_id=self.id) 14 | perm_id = [r.perm_id for r in relations] 15 | return Permission.objects.filter(id__in=perm_id) 16 | 17 | def has_perm(self, perm_name): 18 | '''检查此 VIP 是否具有某种权限''' 19 | for perm in self.perms(): 20 | if perm.name == perm_name: 21 | return True 22 | return False 23 | 24 | 25 | class Permission(models.Model): 26 | ''' 27 | 用户特权 28 | superlike 超级喜欢的权限 29 | rewind 反悔的权限 30 | likeme 查看谁喜欢我的权限 31 | ''' 32 | name = models.CharField(max_length=32) 33 | description = models.TextField(verbose_name='权限详情介绍') 34 | 35 | 36 | class VipPermRelation(models.Model): 37 | ''' 38 | VIP-Permission 关系表 39 | 40 | 每级 VIP 对应的权限 41 | VIP1: 超级喜欢权限 42 | VIP2: 全部 VIP1 的权限 + 反悔权限 43 | VIP3: 全部 VIP2 的权限 + 查看被喜欢权限 44 | 45 | NOTE: 46 | 如果需要可以将权限做的更细,每种权限限制每天的使用次数。 47 | 比如 VIP1 每日反悔 3 次,VIP2 每日反悔 10 次 48 | ''' 49 | vip_id = models.IntegerField() 50 | perm_id = models.IntegerField() 51 | 52 | @classmethod 53 | def add_relation(cls, vip_name, perm_name): 54 | vip = Vip.get(name=vip_name) 55 | perm = Permission.get(name=perm_name) 56 | cls.get_or_create(vip_id=vip_id, perm_id=perm_id) 57 | 58 | @classmethod 59 | def del_relation(cls, vip_name, perm_name): 60 | vip = Vip.get(name=vip_name) 61 | perm = Permission.get(name=perm_name) 62 | try: 63 | cls.get(vip_id=vip_id, perm_id=perm_id).delete() 64 | except cls.DoesNotExist: 65 | pass 66 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/worker/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from celery import Celery 3 | 4 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'swiper.settings') 5 | 6 | # TODO 7 | # 异步上传头像到七牛云 8 | # 异步登录后自动加载数据到 redis 9 | # 异步存储处理 10 | 11 | # 创建 Celery Application 12 | celery_app = Celery('swiper') 13 | celery_app.config_from_object('worker.config') 14 | celery_app.autodiscover_tasks() 15 | 16 | 17 | def call_by_worker(func): 18 | '''将任务在 Celery 中异步执行''' 19 | task = celery_app.task(func) 20 | return task.delay 21 | -------------------------------------------------------------------------------- /tutorial/swiper/backend/worker/config.py: -------------------------------------------------------------------------------- 1 | _redis_url = 'redis://127.0.0.1:6379/0' 2 | 3 | broker_url = _redis_url 4 | broker_pool_limit = 1000 # Borker 连接池,默认是10 5 | 6 | timezone = 'Asia/Shanghai' 7 | accept_content = ['pickle', 'json'] 8 | 9 | task_serializer = 'pickle' 10 | result_expires = 3600 # 任务过期时间 11 | 12 | result_backend = _redis_url 13 | result_serializer = 'pickle' 14 | result_cache_max = 10000 # 任务结果最大缓存数量 15 | 16 | worker_redirect_stdouts_level = 'INFO' 17 | -------------------------------------------------------------------------------- /tutorial/swiper/chat/application.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from tornado.ioloop import IOLoop 4 | from tornado.web import Application 5 | from tornado.options import options 6 | 7 | from django_env import * # 加载 Django 环境 8 | from config import CONFIG 9 | from handler import ChatHandler 10 | from log import logger 11 | 12 | 13 | def main(): 14 | handlers = [('/chatsocket', ChatHandler)] 15 | chat_app = Application(handlers, **CONFIG) 16 | chat_app.listen(options.port) 17 | ChatHandler.globle_listen() 18 | 19 | try: 20 | ioloop = IOLoop.current() 21 | ioloop.start() 22 | except KeyboardInterrupt: 23 | logger.info('Exit') 24 | sys.exit(0) 25 | 26 | 27 | if __name__ == "__main__": 28 | main() 29 | -------------------------------------------------------------------------------- /tutorial/swiper/chat/config.py: -------------------------------------------------------------------------------- 1 | """系统配置""" 2 | from tornado.options import define 3 | 4 | define("port", default="8080", type=int, help="端口号") 5 | define("log_level", default="debug", help="日志等级, 可选项: debug|info|warn|error") 6 | define("log_path", default="chat.log", help="日志路径") 7 | define("log_backup", default=30, type=int, help="日志文件数量") 8 | 9 | CONFIG = { 10 | 'cookie_secret': '\x1az\x11tB4\x17g7[Fry)R\xa1a&"\x7f\x1a/r<\x13,:y\xaeR6M', 11 | 'xsrf_cookies': True, 12 | 'autoreload': True, 13 | 'debug': False, 14 | } 15 | -------------------------------------------------------------------------------- /tutorial/swiper/chat/django_env.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 加载 Django 环境 3 | 4 | 直接在需要的地方 `from django_env import *` 即可 5 | ''' 6 | 7 | import os 8 | import sys 9 | 10 | import django 11 | 12 | __all__ = () 13 | 14 | 15 | CHAT_DIR = os.path.dirname(os.path.abspath(__file__)) 16 | WEB_DIR = os.path.join(os.path.dirname(CHAT_DIR), 'backend') 17 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "swiper.settings") 18 | sys.path.insert(0, WEB_DIR) 19 | django.setup() 20 | -------------------------------------------------------------------------------- /tutorial/swiper/chat/handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from json import dumps, loads 3 | from collections import namedtuple, defaultdict 4 | 5 | from tornadoredis import Client 6 | from tornado.gen import coroutine, Task 7 | from tornado.websocket import WebSocketHandler 8 | 9 | # 从 Web 端引入 10 | from django.conf import settings 11 | from lib.cache import rds 12 | from user.models import User 13 | 14 | # 从 Chat 本身引入 15 | import log 16 | import logic 17 | 18 | Packet = namedtuple('Packet', ['tag', 'data']) 19 | 20 | 21 | class ChatHandler(WebSocketHandler): 22 | '''WebSocket 处理类 23 | 1. 客户端需要先登陆 Web 系统,取得 session id 再来与 Chat Server 建立连接 24 | 2. 连接时 header 中传入用户唯一标识 session_id 25 | 26 | 数据结构 27 | connections: { 28 | uid1: conn_user_1, 29 | uid2: conn_user_2, 30 | } 31 | 32 | 频道格式: "频道名" 或 "频道名:其他标识" 33 | BROADCAST 34 | PRIVATE:uid 35 | ''' 36 | connections = {} 37 | 38 | def __init__(self, application, request, **kwargs): 39 | super().__init__(self, application, request, **kwargs) 40 | self.uid = None 41 | self.user = None 42 | self.sessionid = None 43 | self.rds = None 44 | 45 | def get_compression_options(self): 46 | '''启用压缩''' 47 | return {} 48 | 49 | def get_current_user(self): 50 | '''当前用户''' 51 | return self.user 52 | 53 | def send_to_client(self, tag, data): 54 | '''打包数据, 并发送给客户端''' 55 | packet = Packet(tag, data) 56 | message = logic.pack_msg(packet) 57 | self.write_message(message) 58 | 59 | @staticmethod 60 | def connect_redis(): 61 | rds = Client(host=settings.REDIS['Master']['host'], 62 | port=settings.REDIS['Master']['port'], 63 | selected_db=settings.REDIS['Master']['db']) 64 | rds.connect() 65 | return rds 66 | 67 | @classmethod 68 | def kick_out(cls, uid): 69 | '''强制踢下线''' 70 | old_conn = cls.connections.pop(uid) 71 | old_conn.close(499, 'KickOut') 72 | log.logger.error('KickOut: ip=%s id=%s' % (old_conn.request.remote_ip, uid)) 73 | del old_conn 74 | 75 | def open(self): 76 | '''WebSocket连接完成后的处理''' 77 | # 检查客户端是否已登陆 Web 系统 78 | session_name = settings.SESSION_COOKIE_NAME 79 | sessionid = self.request.headers.get(session_name) 80 | session = logic.get_web_session(sessionid) 81 | if sessionid is None or not session.has_key(session_name): 82 | self.close(403, 'Forbidden') 83 | log.logger.error('Connection refused: %s' % self.request.remote_ip) 84 | return 85 | 86 | try: 87 | # 检查重复登录, 新的登陆会把旧的顶下去 88 | if uid in ChatHandler.connections: 89 | self.kick_out(uid) 90 | 91 | # 保存 connections 92 | self.uid = uid 93 | self.user = User.get(uid=session['uid']) 94 | self.sessionid = sessionid 95 | self.login_time = int(time.time()) 96 | self.rds = self.connect_redis() # 异步redis连接 (用于监听用户相关消息) 97 | ChatHandler.connections[self.uid] = self 98 | 99 | self.pull_history_chat() # 拉取历史消息并回写给 client 100 | self.listen() 101 | except Exception: 102 | log.trace_err() 103 | 104 | @coroutine 105 | def listen(self): 106 | '''监听用户频道''' 107 | def handle_private_msg(msg): 108 | '''处理私人消息''' 109 | try: 110 | log.logger.debug('Receive user msg: %s' % repr(msg)) 111 | if msg.kind == 'message' and msg.channel.startswith('PRIVATE:'): 112 | self.write_message(msg.body) 113 | except Exception: 114 | log.trace_err() 115 | 116 | try: 117 | # 添加订阅 118 | channels = ['PRIVATE:%s' % self.uid] # 私聊 119 | yield Task(self.rds.subscribe, channels) 120 | self.rds.listen(handle_private_msg) 121 | except Exception: 122 | log.trace_err() 123 | 124 | @classmethod 125 | @coroutine 126 | def globle_listen(cls): 127 | ''' 128 | 监听全局频道 129 | 130 | 全局广播频道使用单独的监听器, 收到消息后直接写回客户端 131 | ''' 132 | def handle_global_msg(msg): 133 | '''处理全局消息''' 134 | try: 135 | log.logger.debug('Receive globle msg: %s' % repr(msg)) 136 | if msg.kind == 'message': 137 | # 向所有客户端推送消息 138 | for cli_conn in cls.connections.values(): 139 | cli_conn.write_message(msg.body) 140 | except Exception: 141 | log.trace_err() 142 | 143 | try: 144 | # 检查全局监听器 145 | rds_listener = cls.connect_redis() 146 | 147 | # 开启 PUB/SUB 监听 148 | channels = ['BROADCAST'] 149 | yield Task(rds_listener.subscribe, channels) 150 | rds_listener.listen(handle_global_msg) 151 | except Exception: 152 | log.trace_err() 153 | 154 | def on_message(self, message): 155 | '''处理客户端发来的消息''' 156 | try: 157 | log.logger.debug('Get request: %s' % repr(message)) 158 | packet = Packet(*loads(message)) 159 | except (TypeError, ValueError) as e: 160 | self.send_to_client('ERR', 'DataError') 161 | return 162 | 163 | try: 164 | if packet.tag == 'PRIVATE': 165 | # 将私聊消息发给目标用户 166 | self.private_chat(packet.data['to'], packet.data['msg']) 167 | 168 | elif packet.tag == 'BROADCAST': 169 | # 发送广播 170 | self.broadcast(packet.data) 171 | 172 | else: 173 | # 无法匹配类型 174 | log.logger.error('Can not match the msg: %s' % repr(message)) 175 | except Exception: 176 | log.trace_err() 177 | 178 | @coroutine 179 | def pull_history_chat(self): 180 | '''拉取历史聊天记录''' 181 | try: 182 | # 获取 redis 缓存的消息 183 | with self.rds.pipeline() as pipe: 184 | p_channel = 'PRIVATE:%s' % self.uid 185 | pipe.lrange(p_channel, 0, -1) 186 | pipe.delete(p_channel) 187 | res = yield Task(pipe.execute) 188 | 189 | p_chats = [loads(chat)[1] for chat in res[0]] 190 | self.send_to_client('HISTORY', p_chats) 191 | except Exception: 192 | log.trace_err() 193 | 194 | def pack_chat_msg(self, channel, msg): 195 | '''封装私聊消息''' 196 | data = { 197 | 'tm': int(time.time()), # 时间戳 198 | 'from': self.uid, # 发送者 uid 199 | 'nickname': self.user.nickname, # 昵称 200 | 'avatar': self.user.avatar[0], # 头像 ID 201 | 'msg': msg, # 消息内容 202 | } 203 | packet = Packet(channel, data) 204 | return logic.pack_msg(packet) 205 | 206 | def private_chat(self, to_uid, msg): 207 | '''私人聊天''' 208 | channel = 'PRIVATE:%s' % to_uid 209 | message = self.pack_chat_msg('PRIVATE', msg) 210 | 211 | if rds.hexists(ckeys.AUTH_TKIDX, to_uid): 212 | rds.publish(channel, message) 213 | else: 214 | log.logger.debug('Publish pchat to LIST: %s' % repr(message)) 215 | rds.rpush(channel, message) 216 | 217 | def broadcast(self, msg): 218 | '''广播''' 219 | packet = Packet('BROADCAST', msg) 220 | message = logic.pack_msg(packet) 221 | rds.publish('BROADCAST', message) 222 | 223 | def on_close(self): 224 | try: 225 | # 取消订阅 226 | if getattr(self, 'rds', None) and self.rds.subscribed: 227 | self.rds.unsubscribe(self.rds.subscribed) 228 | self.rds.disconnect() 229 | 230 | if hasattr(self, 'uid'): 231 | # 清理 WebSocket connections 232 | ChatHandler.connections[self.server].pop(self.uid, None) 233 | except Exception as e: 234 | log.trace_err() 235 | -------------------------------------------------------------------------------- /tutorial/swiper/chat/log.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | import traceback 5 | from logging.handlers import TimedRotatingFileHandler 6 | 7 | from tornado import log 8 | from tornado.options import options 9 | 10 | __all__ = ('logger', 'trace_err') 11 | 12 | 13 | def configure_loggers(): 14 | # 获取参数 15 | path = options.log_path 16 | backup = options.log_backup 17 | level = getattr(logging, options.log_level.upper()) 18 | 19 | # 定义日志格式: '时间 级别 [模块名.函数名 ]: message' 20 | fmt = ('%(color)s%(asctime)s %(levelname)5.5s ' 21 | '[%(module)s.%(funcName)s]%(end_color)s: %(message)s') 22 | formatter = log.LogFormatter(datefmt="%Y-%m-%d %H:%M:%S", fmt=fmt) 23 | log_handler = TimedRotatingFileHandler(path, when='D', backupCount=backup) 24 | log_handler.setFormatter(formatter) 25 | 26 | # 设置 handler 27 | for name in ["tornado.application", "tornado.general", "tornado.access"]: 28 | logger = logging.getLogger(name) 29 | logger.setLevel(level) 30 | logger.addHandler(log_handler) 31 | 32 | configure_loggers() 33 | logger = logging.getLogger("tornado.application") 34 | 35 | 36 | def trace_err(): 37 | ''' 38 | 将捕获到的异常信息输出到错误日志 39 | 40 | 直接放到 expect 下即可 41 | 示例: 42 | try: 43 | raise ValueError 44 | except Exception, e: 45 | trace_err() 46 | ''' 47 | split_line = lambda title: '\n%s\n' % title.center(50, '-') 48 | 49 | # 取出格式化的异常信息 50 | msg = split_line(' Error ') 51 | msg += traceback.format_exc() 52 | 53 | # 取出异常位置的参数 54 | msg += split_line(' Args ') 55 | for k, v in sorted(sys._getframe(1).f_locals.items()): 56 | msg += '>>> %s: %s\n' % (k, v) 57 | 58 | logger.error(msg) 59 | -------------------------------------------------------------------------------- /tutorial/swiper/chat/logic.py: -------------------------------------------------------------------------------- 1 | import json 2 | from importlib import import_module 3 | 4 | from django.conf import settings 5 | 6 | 7 | def get_web_session(session_key): 8 | '''获取 Web 端的 session''' 9 | engine = import_module(settings.SESSION_ENGINE) 10 | session = engine.SessionStore(session_key) 11 | return session 12 | 13 | 14 | def pack_msg(packet): 15 | msg = json.dumps(packet, ensure_ascii=False, separators=(',', ':')) 16 | return msg.encode('utf8') 17 | -------------------------------------------------------------------------------- /tutorial/swiper/deployment/README.md: -------------------------------------------------------------------------------- 1 | 1. 服务器初始化配置 2 | - apt update -y 3 | - apt upgrade -y 4 | - 服务器初始配置 5 | 6 | 2. 安装系统依赖库 7 | - apt install BASIC EXT NETWORK LIBS 8 | - install Nginx 9 | - install redis 10 | - install mysql-server 11 | 12 | 3. 安装 Python 13 | 14 | 4. 安装 Python 虚拟环境及依赖库 15 | 16 | 5. 代码发布上线 17 | 18 | 6. 服务启动、停止、重启 19 | -------------------------------------------------------------------------------- /tutorial/swiper/deployment/celery-start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | celery worker -A worker --loglevel=info 4 | -------------------------------------------------------------------------------- /tutorial/swiper/deployment/init.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | import random 6 | 7 | import django 8 | 9 | # 设置环境 10 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 11 | BACKEND_DIR = os.path.join(BASE_DIR, 'backend') 12 | 13 | sys.path.insert(0, BACKEND_DIR) 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "swiper.settings") 15 | django.setup() 16 | 17 | 18 | from user.models import User 19 | 20 | last_names = ( 21 | '赵钱孙李周吴郑王冯陈褚卫蒋沈韩杨' 22 | '朱秦尤许何吕施张孔曹严华金魏陶姜' 23 | '戚谢邹喻柏水窦章云苏潘葛奚范彭郎' 24 | '鲁韦昌马苗凤花方俞任袁柳酆鲍史唐' 25 | '费廉岑薛雷贺倪汤滕殷罗毕郝邬安常' 26 | '乐于时傅皮卞齐康伍余元卜顾孟平黄' 27 | ) 28 | 29 | first_names = { 30 | 'Male': [ 31 | '致远', '俊驰', '雨泽', '烨磊', '晟睿', 32 | '天佑', '文昊', '修洁', '黎昕', '远航', 33 | '旭尧', '鸿涛', '伟祺', '荣轩', '越泽', 34 | '浩宇', '瑾瑜', '皓轩', '浦泽', '绍辉', 35 | '绍祺', '升荣', '圣杰', '晟睿', '思聪' 36 | ], 37 | 'Female': [ 38 | '沛玲', '欣妍', '佳琦', '雅芙', '雨婷', 39 | '韵寒', '莉姿', '雨婷', '宁馨', '妙菱', 40 | '心琪', '雯媛', '诗婧', '露洁', '静琪', 41 | '雅琳', '灵韵', '清菡', '溶月', '素菲', 42 | '雨嘉', '雅静', '梦洁', '梦璐', '惠茜' 43 | ] 44 | } 45 | 46 | 47 | def rand_name(): 48 | last_name = random.choice(last_names) 49 | sex = random.choice(['Male', 'Female']) 50 | first_name = random.choice(first_names[sex]) 51 | return ''.join([last_name, first_name]), sex 52 | 53 | 54 | # 创建初始用户 55 | for i in range(1000): 56 | name, sex = rand_name() 57 | User.objects.create( 58 | phonenum='%s' % random.randrange(21000000000, 21900000000), 59 | nickname=name, 60 | sex=sex, 61 | birth_year=random.randint(1980, 2000), 62 | birth_month=random.randint(1, 12), 63 | birth_day=random.randint(1, 28), 64 | location=random.choice(['北京', '上海', '深圳', '成都', '西安', '沈阳', '武汉']), 65 | ) 66 | print('created: %s %s' % (name, sex)) 67 | -------------------------------------------------------------------------------- /tutorial/swiper/deployment/nginx.conf: -------------------------------------------------------------------------------- 1 | user root; 2 | worker_processes 4; 3 | pid /run/nginx.pid; 4 | 5 | events { 6 | worker_connections 10240; 7 | } 8 | 9 | http { 10 | include mime.types; 11 | default_type application/octet-stream; 12 | 13 | log_format main '$time_local $remote_addr $status $request_time ' 14 | '$request [$body_bytes_sent/$bytes_sent] ' 15 | '"$http_user_agent" "$http_referer"'; 16 | 17 | sendfile on; 18 | tcp_nopush on; 19 | keepalive_timeout 65; 20 | gzip on; 21 | 22 | upstream app_server { 23 | server 127.0.0.1:8000 weight=10; 24 | server 127.0.0.1:9000 weight=10; 25 | } 26 | 27 | server { 28 | listen 80; 29 | server_name swiper.seamile.org; 30 | 31 | access_log /opt/swiper/logs/access.log main; 32 | error_log /opt/swiper/logs/error.log; 33 | 34 | location = /favicon.ico { 35 | empty_gif; 36 | access_log off; 37 | } 38 | 39 | location /static/ { 40 | root /opt/swiper/frontend/; 41 | expires 30d; 42 | access_log off; 43 | } 44 | 45 | location / { 46 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 47 | proxy_set_header Host $http_host; 48 | proxy_redirect off; 49 | proxy_pass http://app_server; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tutorial/swiper/deployment/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | LOCAL_DIR="./" 4 | REMOTE_DIR="/opt/swiper" 5 | 6 | USER='root' 7 | HOST='35.194.171.19' 8 | 9 | # 上传代码 10 | rsync -crvP --exclude={.git,.venv,__pycache__} $LOCAL_DIR $USER@$HOST:$REMOTE_DIR/ 11 | 12 | # 远程重启 13 | ssh $USER@$HOST "$REMOTE_DIR/deployment/restart.sh" 14 | -------------------------------------------------------------------------------- /tutorial/swiper/deployment/restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROJECT="/opt/swiper" 4 | 5 | cat $PROJECT/backend/logs/gunicorn.pid | xargs kill -HUP 6 | -------------------------------------------------------------------------------- /tutorial/swiper/deployment/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 系统更新 4 | system_update() { 5 | echo '正在更新系统...' 6 | apt update -y 7 | apt upgrade -y 8 | echo -e '系统更新完毕.\n' 9 | } 10 | 11 | 12 | # 安装系统软件 13 | install_software() { 14 | echo '正在安装系统组件...' 15 | BASIC='man gcc make sudo lsof ssh openssl tree vim language-pack-zh-hans' 16 | EXT='dnsutils iputils-ping net-tools psmisc sysstat' 17 | NETWORK='curl telnet traceroute wget' 18 | LIBS='libbz2-dev libpcre3 libpcre3-dev libreadline-dev libsqlite3-dev libssl-dev zlib1g-dev' 19 | SOFTWARE='git mysql-server zip p7zip apache2-utils sendmail' 20 | apt install -y $BASIC $EXT $NETWORK $LIBS $SOFTWARE 21 | 22 | echo '正在清理临时文件' 23 | apt autoremove 24 | apt autoclean 25 | 26 | echo '正在设置中文环境' 27 | locale-gen zh_CN.UTF-8 28 | export LC_ALL='zh_CN.utf8' 29 | echo "export LC_ALL='zh_CN.utf8'" >> /etc/bash.bashrc 30 | 31 | echo '正在启动邮件服务' 32 | service sendmail start 33 | 34 | echo -e '系统组件安装完毕.\n' 35 | } 36 | 37 | 38 | # 安装 Nginx 39 | install_nginx() { 40 | echo '正在安装 Nginx...' 41 | if ! which nginx > /dev/null 42 | then 43 | wget -P /tmp 'http://nginx.org/download/nginx-1.14.1.tar.gz' 44 | tar -xzf /tmp/nginx-1.14.1.tar.gz -C /tmp 45 | cd /tmp/nginx-1.14.1 46 | ./configure 47 | make 48 | make install 49 | cd - 50 | rm -rf /tmp/nginx* 51 | ln -s /usr/local/nginx/sbin/nginx /usr/local/bin/nginx 52 | echo -e 'Nginx 安装完毕.\n' 53 | else 54 | echo -e 'Nginx 已存在.\n' 55 | fi 56 | } 57 | 58 | 59 | # 安装 Redis 60 | install_redis() { 61 | echo '正在安装 Redis' 62 | if ! which redis-server > /dev/null 63 | then 64 | wget -P /tmp/ 'http://download.redis.io/releases/redis-5.0.0.tar.gz' 65 | tar -xzf /tmp/redis-5.0.0.tar.gz -C /tmp 66 | cd /tmp/redis-5.0.0 67 | make && make install 68 | cd - 69 | rm -rf /tmp/redis* 70 | echo -e 'Redis 安装完毕.\n' 71 | else 72 | echo -e 'Redis 已存在\n' 73 | fi 74 | } 75 | 76 | 77 | # 安装 pyenv 78 | install_pyenv() { 79 | echo '正在安装 pyenv...' 80 | if ! which pyenv > /dev/null 81 | then 82 | curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash 83 | export PATH="$HOME/.pyenv/bin:$PATH" 84 | eval "$(pyenv init -)" 85 | eval "$(pyenv virtualenv-init -)" 86 | echo -e 'pyenv 安装完毕.\n' 87 | else 88 | echo -e 'pyenv 已存在\n' 89 | fi 90 | pyenv update 91 | } 92 | 93 | 94 | # 将 pyenv 配置写入 bashrc 95 | set_pyenv_conf() { 96 | echo '正在配置 pyenv...' 97 | cat >> $HOME/.bashrc << EOF 98 | 99 | # PyenvConfig 100 | export PATH="\$HOME/.pyenv/bin:\$PATH" 101 | eval "\$(pyenv init -)" 102 | eval "\$(pyenv virtualenv-init -)" 103 | EOF 104 | 105 | source $HOME/.bashrc 106 | echo -e 'pyenv 配置完毕.\n' 107 | } 108 | 109 | 110 | # 编译安装 Python 3.6.7 111 | install_python() { 112 | echo '正在安装 Python 3.6' 113 | if ! pyenv versions|grep 3.6.7 > /dev/null; 114 | then 115 | pyenv install -v 3.6.7 116 | echo -e 'Python 3.6.7 安装完毕.\n' 117 | else 118 | echo 'Python 3.6.7 已存在' 119 | fi 120 | pyenv global 3.6.7 121 | } 122 | 123 | 124 | # 项目环境初始化 125 | project_init() { 126 | echo '正在设置项目环境...' 127 | proj='/opt/swiper/' 128 | mkdir -p $proj/{backend,frontend,deployment,data,logs} 129 | 130 | echo '正在创建 python 运行环境...' 131 | if [ ! -d $proj/.venv ]; then 132 | python -m venv $proj/.venv 133 | fi 134 | source $proj/.venv/bin/activate 135 | pip install -U pip 136 | if [ -f $proj/requirements.txt ]; then 137 | pip install -r $proj/requirements.txt 138 | fi 139 | deactivate 140 | 141 | echo -e '项目环境设置完毕.\n' 142 | } 143 | 144 | install_all() { 145 | system_update 146 | install_software 147 | install_nginx 148 | install_redis 149 | install_pyenv 150 | set_pyenv_conf 151 | install_python 152 | project_init 153 | } 154 | 155 | 156 | cat << EOF 157 | 请输入要执行的操作的编号: [1-9] 158 | =============================== 159 | 【 1 】 系统更新 160 | 【 2 】 安装系统组件 161 | 【 3 】 安装 Nginx 162 | 【 4 】 安装 Redis 163 | 【 5 】 安装 Pyenv 164 | 【 6 】 写入 pyenv 配置 165 | 【 7 】 安装 Python 166 | 【 8 】 项目运行环境初始化 167 | 【 9 】 全部执行 168 | 【 Q 】 退出 169 | =============================== 170 | EOF 171 | 172 | if [[ -n $1 ]]; then 173 | input=$1 174 | echo "执行操作: $1" 175 | else 176 | read -p "请选择: " input 177 | fi 178 | 179 | case $input in 180 | 1) system_update;; 181 | 2) install_software;; 182 | 3) install_nginx;; 183 | 4) install_redis;; 184 | 5) install_pyenv;; 185 | 6) set_pyenv_conf;; 186 | 7) install_python;; 187 | 8) project_init;; 188 | 9) install_all;; 189 | *) exit;; 190 | esac 191 | -------------------------------------------------------------------------------- /tutorial/swiper/deployment/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROJECT="/opt/swiper" 4 | 5 | cd $PROJECT/backend 6 | source $PROJECT/.venv/bin/activate 7 | gunicorn -c swiper/gunicorn-config.py swiper.wsgi 8 | deactivate 9 | cd - 10 | -------------------------------------------------------------------------------- /tutorial/swiper/deployment/stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROJECT="/opt/swiper" 4 | 5 | # 关掉 gunicorn 6 | cat $PROJECT/backend/logs/gunicorn.pid | xargs kill 7 | -------------------------------------------------------------------------------- /tutorial/swiper/doc/4.1.1-团队构建及项目管理.md: -------------------------------------------------------------------------------- 1 | # 4.1.1 团队构建及项目管理 2 | 3 | ## 项目概览 4 | 5 | Swiper Social 是一个类似于 “探探” 的社交类程序, 采用前后端分离结构, 主要包含以下模块: 6 | 7 | 1. 个人模块 8 | 2. 社交模块 9 | 3. VIP 模块 10 | 4. 异步任务模块 11 | 5. Redis 缓存模块 12 | 6. 日志模块、异常处理模块 13 | 7. 短信模块、邮件模块 14 | 8. 运维、部署、shell 脚本 15 | 9. 前端模块 16 | 10. 其他 17 | 18 | 19 | ## 项目目标 20 | 21 | 1. 了解真实项目的开发流程 22 | 2. 掌握如何使用 Git 完成协作开发和代码管理 23 | 3. 掌握 RESTful 的概念, 掌握前后端分离式的开发 24 | 4. 掌握日志的使用 25 | 5. 掌握缓存的使用 26 | 6. 掌握 Redis 不同数据类型的用法 27 | 7. 掌握 Celery 异步任务处理 28 | 8. 掌握 Nginx 的配置, 及负载均衡的原理 29 | 9. 了解分布式数据库及数据分片 30 | 10. 掌握数据库关系建模, 及不使用外键如何构建关系 31 | 11. 掌握服务器异常处理, 及报警处理 32 | 12. 熟练掌握常用 Linux 命令, 以及初级 bash 脚本的开发 33 | 13. 掌握线上服务器的安装、部署 34 | 14. 理解进程、线程、协程的原理, 以及多路复用、事件驱动、异步非阻塞等概念 35 | 15. 对服务器架构、服务高可用等有一个初步认识 36 | 37 | 38 | ## 企业中的团队建制 39 | 40 | - 管理层 41 | 42 | - 高层:CEO、COO、CTO 等 43 | - 中层:各部门总监、经理 44 | - 基层:主程、Leader 45 | 46 | - 人力部门 47 | 48 | - 制定用人制度, 负责人员的流入流出 49 | - 制定绩效考核制度, 审批薪酬表 50 | - **每个求职者都要经过人力面试** 51 | 52 | - 行政部门 53 | 54 | - 日常办公、卫生管理, 会议、活动管理 55 | - 内部物品、设备的预算和购置 56 | 57 | - 财务部门 58 | 59 | - 资产管理、预算及成本管理、风险管控 60 | - 薪酬管理, 税务、财报管理 61 | 62 | - 市场部门 / 运营部门 63 | 64 | - 研发部门 65 | 66 | - 项目经理 67 | - 产品研发 68 | - 产品人员 69 | - 设计人员 70 | - 技术研发 71 | - 技术总监 72 | - 前端开发 73 | - HTML5 (3~4人) 74 | - iOS (3~4人) 75 | - Android (3~4人) 76 | - 后端开发 77 | - Python / PHP / Java / Go (4~8人) 78 | - 运维 79 | - DBA 80 | - 测试 81 | - 白盒测试 82 | - 黑盒测试 (1~2) 83 | 84 | 85 | ## 工作中的开发流程 86 | 87 | 1. 产品人员进行原型设计, 提出开发需求 88 | 2. 产品需求讨论会 89 | 3. 设计人员进行 UI、原画等绘制工作 90 | 4. 前端人员接收各种图形元素 91 | 5. 前后端人员对接接口, 并编写接口文档 92 | 6. 前后端同时开始开发 93 | 7. 前后端联合调试 94 | 8. 测试人员测试 95 | 9. 上线部署、服务重启 96 | 10. 新版本发布上线 97 | 98 | 99 | ## 项目阶段开发流程及要求 100 | 101 | 1. 两人一组, 结组编程, 每组不要超过三人 102 | 2. 每组选一人作为组长, 由组长在 Github 上创建自己的组和项目 103 | 3. 组长分配任务, 各自开发自己的功能 104 | 4. 开发过程中注意编码规范, 力求做到 "团队代码如同一人编写" 105 | 5. 每个人为接到的功能创建一个独立的分支 106 | 6. 开发、提交、审核、合并、上线 107 | 108 | 109 | ## Git 命令回顾 110 | 111 | ![git](./img/git.png) 112 | 113 | - **`init`**: 在本地创建一个新的库 114 | - **`clone`**: 从服务器克隆代码到本地 (将所有代码下载) 115 | - **`status`**: 查看当前代码库的状态 116 | - **`add`**: 将本地文件添加到暂存区 117 | - **`commit`**: 将代码提交到本地仓库 118 | - **`push`**: 将本地代码推送到远程仓库 119 | - **`pull`**: 将远程仓库的代码拉取到本地 (只更新与本地不一样的代码) 120 | - **`branch`**: 分支管理 121 | - **`checkout`**: 切换分支 / 代码回滚 / 代码还原 122 | - **`merge`**: 合并分支 123 | - **`log`**: 查看提交历史 124 | - **`diff`**: 差异对比 125 | - **`remote`**: 远程库管理 126 | - **`.gitignore`**: 一个特殊文件, 用来记录需要忽略哪些文件 127 | - ssh-key 的使用 128 | 129 | 130 | ## Github Flow 131 | 132 | 1. 版本控制及代码管理 133 | 134 | - 分支类型 135 | 136 | - master: 主干分支, 代码经过严格测试, 最稳定, 可以随时上线 137 | - develop: 开发分支, 合并了各个开发者最新完成的功能, 经过了初步测试, 没有明显 BUG 138 | - feature: 功能分支, 开发中的状态, 代码最不稳定, 开发完成后需要合并到 develop 分支 139 | 140 | - Pull Request: 拉去请求 141 | 142 | - 开发者自己提交 Pull Request 通知团队成员来合并自己提交的代码。 143 | - 通过此方式可以将合并过程暴露给团队成员, 让代码在合并之前可以被团队其他成员审核, 保证代码质量。 144 | 145 | - Code Review: 代码审核 146 | - 代码逻辑问题 147 | - 算法问题 148 | - 错误的使用方式 149 | - 代码风格及规范化问题 150 | - **学习其他人的优秀代码** 151 | 152 | 2. 上线流程介绍 153 | 154 | ``` 155 | 生产环境服务器 156 | ^ 157 | | 自动化部署 158 | | 1. 代码发布上线 159 | 0.1 1.0 2.0 3.0 3.2 | 2. 服务自动重启 160 | master *------*-------*--------------*------------*-------------> 161 | | ^ 2. 合并 162 | \ | 1. 发布到测试服 163 | develop *---------------------------*----*-------*-------------> 164 | |\ ^ \ ^ 165 | | \ | \ | 166 | | V | V | 167 | A: user | *------*-----------*----|---*------->* 168 | | | 4. 合并 (Merge) 169 | \ | 3. 团队成员进行 “Code Review” 170 | V | 2. 发起 “Pull Request” 171 | B: post *---*---*---*-----*------>* 1. 开发者 B 在自己本地完成测试 172 | ``` 173 | 174 | 175 | ## 大型项目代码布局 176 | 177 | 1. 概览 178 | 179 | ``` 180 | proj/ 181 | ├── proj/ 182 | │ ├── settings.py 183 | │ ├── other_config.py # 其他配置 184 | │ ├── urls.py 185 | │ └── wsgi.py 186 | ├── common/ # 不与具体模块关联的独立的东西写到这里 187 | │ ├── errors.py 188 | │ ├── keys.py 189 | │ └── middleware.py 190 | ├── app1/ 191 | │ ├── migrations/ 192 | │ ├── apps.py 193 | │ ├── helper.py (logic.py) # 逻辑写到这里 194 | │ ├── models.py 195 | │ └── views.py (api.py) 196 | ├── app2/ 197 | │ ├── migrations/ 198 | │ ├── apps.py 199 | │ ├── helper.py 200 | │ ├── models.py 201 | │ └── views.py (api.py) 202 | ├── lib/ # 底层模块写到这里 203 | │ ├── cache.py 204 | │ ├── http.py 205 | │ ├── orm.py 206 | │ └── sms.py 207 | ├── worker/ # 异步任务,或耗时任务,或定时任务 208 | │ ├── __init__.py 209 | │ └── config.py 210 | └── manage.py 211 | ``` 212 | 213 | 2. 布局详解 214 | 215 | - 通用的算法、功能放到 common 目录 216 | - 底层的功能放到 lib 目录 217 | - 独立脚本的放到 scripts 目录 218 | - 配置文件放到项目目录 或 config 目录 219 | - views.py 及 view_func() 220 | 1. MVC 模式的 V 只负责试图处理, 逻辑属于 Controller 层 221 | 2. view_func 本身不适合写逻辑, view 是特殊函数, 只负责视图处理。 222 | 3. 添加 helper.py 文件, 用来放置每个 app 的逻辑函数 223 | 4. 函数构建应保持功能单一, 一个函数只做一件事情, 并把它做好, 避免构建复杂函数 224 | 5. 复杂功能通过不同函数组合完成 225 | 226 | 227 | ## 项目初始化 228 | 229 | ```bash 230 | $ mkdir demo 231 | $ cd demo 232 | $ cat > .gitignore << EOF 233 | *.pyc 234 | *.sqlite3 235 | .idea 236 | __pycache__ 237 | *.log 238 | .venv 239 | medias/* 240 | EOF 241 | $ python -m venv .venv 242 | $ source .venv/bin/activate 243 | $ pip install ipython django==1.11.7 redis django-redis gevent gunicorn requests celery 244 | $ pip freeze > requirements.txt 245 | $ django-admin startproject demo ./ 246 | $ git init 247 | $ git add ./ 248 | $ git commit -m 'first commit' 249 | $ git remote add origin git@github.com:yourname/demo.git 250 | $ git push -u origin master 251 | ``` 252 | -------------------------------------------------------------------------------- /tutorial/swiper/doc/4.1.2-User功能开发.md: -------------------------------------------------------------------------------- 1 | # 4.1.2 User 功能开发 2 | 3 | ## "用户中心" 模块功能概览 4 | 5 | - 获取短信验证码 6 | - 通过验证码登录、注册 7 | - 获取个人资料 8 | - 修改个人资料 9 | - 头像上传 10 | 11 | 12 | ## User 模型及接口规划 13 | 14 | 1. User 模型设计 (**仅作参考**) 15 | 16 | | Field | Description | 17 | | ----------- | ----------- | 18 | | phonenum | 手机号 | 19 | | nickname | 昵称 | 20 | | sex | 性别 | 21 | | birth_year | 出生年 | 22 | | birth_month | 出生月 | 23 | | birth_day | 出生日 | 24 | | avatar | 个人形象 | 25 | | location | 常居地 | 26 | 27 | 2. 接口规划 28 | 29 | - 接口1: 提交手机号,发送短信验证码 30 | - 接口2: 提交验证码,登录注册 31 | 32 | 33 | ## 开发中的难点 34 | 35 | 1. 如何向前端返回 json 数据 36 | 2. 短信发送如何处理 37 | 3. 验证码如何处理 38 | - 验证码需随机产生,登录注册验证码一般为 4~6 位随机数 39 | - 第一个接口获取到的验证码在登录接口还需要使用,需如何保存 40 | - 每个验证码都有有效期,应如何处理 41 | 42 | 43 | ## RESTful 与前后端分离 44 | 45 | 1. RESTful 46 | 47 | - RESTful 是一种网络软件架构风格, 而非标准 48 | - 用 URL 定位一个网络资源 49 | - 用 HTTP 动词描述对资源的操作 50 | - GET: 用来获取资源 51 | - POST: 用来新建资源 52 | - PUT: 用来更新资源 53 | - DELETE: 用来删除资源 54 | - 误区 55 | - URL 中使用动词 56 | - URL 中出现版本号 57 | - 参数用 querystring 表示, 而不要拼在 path 部分 58 | - 错误示范: GET /user/book/id/3 59 | - 正确示范: GET /user/book?id=3 60 | - 状态码的使用要精确 61 | - 2xx:操作成功 62 | - 3xx:重定向 63 | - 4xx:客户端错误 64 | - 5xx:服务器错误 65 | - RESTful 与 Django REST framework 的区别 66 | 67 | 2. 前后端分离 68 | 69 | ![front-back](./img/front-back.jpg) 70 | 71 | 传统 Web 开发, view 函数中需要进行模版渲染, 逻辑处理与显示的样式均需要后端开发. 72 | 73 | 变成前后端分离后, 显示效果的处理完全交给前端来做, 前端自由度变大. 后端只需要传递前端需要的数据即可, 将后端人员从繁琐的显示处理中解放出来, 专心处理业务逻辑 74 | 75 | - 优点: 前端负责显示, 后端负责逻辑, 分工更加明确, 彻底解放前、后端开发者 76 | - JSON: 完全独立于编程语言的文本格式, 用来存储和表示数据 77 | - 前后端分离后的开发流程 78 | 79 | ![fb-dev](./img/fb-dev.jpg) 80 | 81 | 3. 代码实现 82 | 83 | ```python 84 | from json import dumps 85 | 86 | from django.http import HttpResponse 87 | 88 | def render_json(data=None, error_code=0): 89 | '''将返回值渲染为 JSON 数据''' 90 | result = { 91 | 'data': data, # 返回给前端的数据 92 | 'code': error_code # 状态码 (status code) 93 | } 94 | 95 | json_str = dumps(result, ensure_ascii=False, separators=[',', ':']) 96 | return HttpResponse(json_str) 97 | ``` 98 | 99 | 4. 接口的定义 100 | 101 | 1. 定义接口基本格式 102 | 103 | ```json 104 | { 105 | "code": 0, // 错误码 (status code) 106 | "data": { // 接口数据 107 | "user": { 108 | "uid": 123321, 109 | "username": "Lion", 110 | "age": 21, 111 | "sex": "Male" 112 | }, 113 | "date": "2018-09-12", 114 | } 115 | } 116 | ``` 117 | 118 | 2. 定义 status 状态码 119 | 120 | | code | description | 121 | | ---- | -------------- | 122 | | 0 | 正常 | 123 | | 1000 | 服务器内部错误 | 124 | | 1001 | 参数错误 | 125 | | 1002 | 数据错误 | 126 | 127 | 3. 详细定义每一个接口的各个部分: 128 | - 名称 (Name) 129 | - 描述 (Description) 130 | - 方法 (Method) 131 | - 路径 (Path) 132 | - 参数 (Params) 133 | - 返回值 (Returns) 134 | 135 | 4. 接口定义举例: 136 | 137 | > **接口名称:提交验证码登录** 138 | > 139 | > * **Description**: 根据上一步的结果提交需要的数据 140 | > * **Method**: POST 141 | > * **Path**: /user/login 142 | > * **Params**: 143 | > 144 | > field | required | type | description 145 | > ------|----------|------|----------------------- 146 | > phone | Yes | int | 手机号 147 | > code | Yes | int | 验证码 148 | > 149 | > * **Return**: 150 | > 151 | > field | required | type | description 152 | > ----------|----------|------|----------------------- 153 | > uid | Yes | int | 用户 id 154 | > nickname | Yes | str | 用户名 155 | > age | Yes | int | 年龄 156 | > sex | Yes | str | 性别 157 | > location | Yes | str | 常居地 158 | > avatars | Yes | list | 头像 URL 列表, 最多为 6 张 159 | > 160 | > 示例: 161 | > ```json 162 | > { 163 | > "code": 0, 164 | > "data": { 165 | > "uid": 123, // 用户 id 166 | > "nickname": "Miao", // 用户名 167 | > "age": 21, // 年龄 168 | > "sex": "M", // 性别 169 | > "location": "China/Beijing", // 常居地 170 | > "avatars": "http://xxx.com/icon/1.jpg" // 头像 171 | > }, 172 | > } 173 | > ``` 174 | 175 | 176 | ## 第三方短信平台的接入 177 | 178 | 1. 短信验证整体流程: 179 | 1. 用户调用应用服务器 "获取验证码接口" (点击 "获取验证码" 按钮时触发) 180 | 2. 应用服务器调用短信平台接口, 将用户手机号和验证码发送到短信平台 181 | 3. 短信平台向用户发送短信 182 | 4. 用户调用 "提交验证码接口",向应用服务器进行验证 183 | 5. 验证通过,登录、注册…… 184 | 185 | 2. 可选短信平台 186 | - 阿里云: 187 | - 腾讯云: 188 | - 网易云: 189 | - 云之讯: 190 | - 互亿无线: 191 | 192 | 3. 注册账号后, 将平台分配的 APP_ID 和 APP_SECRET 添加到配置中 193 | - APP_ID: 平台分配的 ID 194 | - APP_SECRET: 与平台交互时, 用来做安全验证的一段加密用的文本, **不能泄漏给其他人** 195 | 196 | 4. 注册平台的短信模版 197 | 198 | 5. 按照平台接口文档开发接口 199 | - 短信平台的接口通常是 HTTP 或 HTTPS 协议, 接入的时候只需按照接口格式发送 HTTP 请求即可 200 | - 接口的返回值一般为 json 格式,收到返回结果后需要解析 201 | 202 | 203 | ## Django 中的缓存 204 | 205 | 1. 接口及用法 206 | 207 | ```python 208 | from django.core.cache import cache 209 | 210 | # 在缓存中设置 age = 123, 10秒过期 211 | cache.set('age', 123, 10) 212 | 213 | # 获取 age 214 | a = cache.get('age') 215 | print(a) 216 | 217 | # 自增 218 | x = cache.incr('age') 219 | print(x) 220 | ``` 221 | 222 | 2. 使用 Redis 做缓存后端 223 | - 安装 `pip install django-redis` 224 | - settings 配置 225 | 226 | ```python 227 | CACHES = { 228 | "default": { 229 | "BACKEND": "django_redis.cache.RedisCache", 230 | "LOCATION": "redis://127.0.0.1:6379/0", 231 | "OPTIONS": { 232 | "CLIENT_CLASS": "django_redis.client.DefaultClient", 233 | "PICKLE_VERSION": -1, 234 | } 235 | } 236 | } 237 | ``` 238 | 239 | 3. 利用过期时间可以处理一些定时失效的临时数据, 比如手机验证码 240 | 241 | 242 | ## Cookie、 Session 机制剖析 243 | 244 | 1. 产生过程 245 | 246 | 1. 浏览器: 向服务器发送请求 247 | 2. 服务器: 接受并创建 session 对象 (该对象包含一个 session_id) 248 | 3. 服务器: 执行 views 函数, 并得到一个 response 对象 249 | 4. 服务器: 执行 response.set_cookie('sessionid', session_id) 将 session_id 写入 cookie 250 | 5. 服务器: 将 response 传回浏览器 251 | 6. 浏览器: 读取 response 报文, 从 Cookies 取出 session_id 并保存 252 | 253 | 2. 后续请求 254 | 255 | 1. 浏览器: 向服务器发送请求, session_id 随 Cookies 一同发给 Server 256 | 2. 服务器: 从 Headers 的 Cookies 中取出 session_id 257 | 3. 服务器: 根据 session_id 找出对应的数据, 确认客户端身份 258 | 259 | 3. Django 中的代码实现 260 | 261 | ```python 262 | class SessionMiddleware(MiddlewareMixin): 263 | def __init__(self, get_response=None): 264 | self.get_response = get_response 265 | engine = import_module(settings.SESSION_ENGINE) 266 | self.SessionStore = engine.SessionStore # 设置 Session 存储类 267 | 268 | def process_request(self, request): 269 | # 从 Cookie 获取 sessionid 270 | session_key = request.COOKIES.get('session_id') 271 | 272 | # 通过 session_key 获取之前保存的数据 273 | request.session = self.SessionStore(session_key) 274 | 275 | def process_response(self, request, response): 276 | try: 277 | # View 函数结束后, 获取 session 状态 278 | accessed = request.session.accessed 279 | modified = request.session.modified 280 | empty = request.session.is_empty() 281 | except AttributeError: 282 | pass 283 | else: 284 | # 如果 Cookie 中有 sessionid, 但 session 为空, 285 | # 说明 view 中执行过 session.flush 等操作, 286 | # 直接删除 Cookie 中的 session 287 | if 'session_id' in request.COOKIES and empty: 288 | response.delete_cookie( 289 | settings.SESSION_COOKIE_NAME, 290 | path=settings.SESSION_COOKIE_PATH, 291 | domain=settings.SESSION_COOKIE_DOMAIN, 292 | ) 293 | else: 294 | if accessed: 295 | patch_vary_headers(response, ('Cookie',)) 296 | if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty: 297 | # 设置过期时间 298 | if request.session.get_expire_at_browser_close(): 299 | max_age = None 300 | expires = None 301 | else: 302 | max_age = request.session.get_expiry_age() 303 | expires_time = time.time() + max_age 304 | expires = cookie_date(expires_time) 305 | 306 | # 保存会话数据, 并刷新客户端 Cookie 307 | if response.status_code != 500: 308 | try: 309 | request.session.save() 310 | except UpdateError: 311 | raise SuspiciousOperation( 312 | "The request's session was deleted before the " 313 | "request completed. The user may have logged " 314 | "out in a concurrent request, for example." 315 | ) 316 | 317 | # 让客户端将 sessionid 添加到 Cookie 中 318 | response.set_cookie( 319 | 'session_id', 320 | request.session.session_key, 321 | max_age=max_age, 322 | expires=expires, 323 | domain=settings.SESSION_COOKIE_DOMAIN, 324 | path=settings.SESSION_COOKIE_PATH, 325 | secure=settings.SESSION_COOKIE_SECURE or None, 326 | httponly=settings.SESSION_COOKIE_HTTPONLY or None, 327 | ) 328 | return response 329 | ``` 330 | 331 | 332 | ## 作业:登录验证中间件 333 | 334 | 大多数接口都需要登陆后才可使用,可以通过中间件的方式统一验证。 335 | 336 | 要点: 337 | 338 | 1. 统一验证所有接口的登录情况,如果没有登录给出一些提示 339 | 2. 需要将不需要验证的特殊接口排除在外 340 | -------------------------------------------------------------------------------- /tutorial/swiper/doc/4.1.3-个人资料功能开发.md: -------------------------------------------------------------------------------- 1 | # 4.1.3 个人资料功能开发 2 | 3 | ## 个人资料接口规划 4 | 5 | 1. 获取个人资料接口 6 | 2. 修改个人资料接口 7 | 3. 上传个人头像接口 8 | 9 | 10 | ## Profile 模型设计 (仅作参考) 11 | 12 | | Field | Description | 13 | | -------------- | ------------------------ | 14 | | location | 目标城市 | 15 | | min_distance | 最小查找范围 | 16 | | max_distance | 最大查找范围 | 17 | | min_dating_age | 最小交友年龄 | 18 | | max_dating_age | 最大交友年龄 | 19 | | dating_sex | 匹配的性别 | 20 | | vibration | 开启震动 | 21 | | only_matche | 不让为匹配的人看我的相册 | 22 | | auto_play | 自动播放视频 | 23 | 24 | 25 | ## 开发中的难点 26 | 27 | 1. Profile 与 User 两个模型是什么关系 ? 28 | 2. 企业中不使用外键如何构建 "表关联" ? 29 | 3. 接口中有太多字段批量提交时应如何验证 ? 30 | 4. 如何上传头像 ? 31 | 5. 大型项目中如何保存大量的静态文件 ? 32 | 6. 上传文件、发送验证码、图像处理等较慢操作应如何处理才能让用户等待时间更短 ? 33 | 34 | 35 | ## 数据库表关系的构建 36 | 37 | 1. 关系分类 38 | - 一对一关系 39 | - 一对多关系 40 | - 多对多关系 41 | 42 | 2. 外键的优缺点 43 | - 优点: 44 | - 由数据库自身保证数据一致性和完整性, 数据更可靠 45 | - 可以增加 ER 图的可读性 46 | - 外键可节省开发量 47 | - 缺点: 48 | - 性能缺陷, 有额外开销 49 | - 主键表被锁定时, 会引发外键对应的表也被锁 50 | - 删除主键表的数据时, 需先删除外键表的数据 51 | - 修改外键表字段时, 需重建外键约束 52 | - 不能用于分布式环境 53 | - 不容易做到数据解耦 54 | 55 | 3. 应用场景 56 | - 适用场景: 内部系统、传统企业级应用可以使用 (需要数据量可控, 数据库服务器数量可控) 57 | - 不适用场景: 互联网行业不建议使用 58 | 59 | 4. 手动构建关联 60 | 1. 一对多: 主表 id 与 子表 id 完全一一对应 61 | 2. 一对多: 在 "多" 的表内添加 "唯一" 表 id 字段 62 | 3. 多对多: 创建关系表, 关系表中一般只存放两个相关联的条目的 id 63 | 4. 博客案例思考 64 | 1. 用户和文字的关系 65 | 2. 用户和收藏关系 66 | 3. 用户-角色-权限关系 67 | 68 | 5. 可通过 `property` 的方式对子表进行关联操作 69 | 70 | - property 用法 71 | 72 | ```python 73 | class Box: 74 | def __init__(self): 75 | self.l = 123 76 | self.w = 10 77 | self.h = 80 78 | 79 | @property 80 | def V(self): 81 | return self.l * self.w * self.h 82 | 83 | b = Box() 84 | print(b.V) 85 | ``` 86 | 87 | - 对子表关联操作 88 | 89 | ```python 90 | class User(models.Model): 91 | ... 92 | demo_id = models.IntegerField() 93 | ... 94 | 95 | @property 96 | def demo(self): 97 | if not hasattr(self, '_demo'): 98 | self._demo = Demo.objects.get(id=self.demo_id) 99 | return self._demo 100 | 101 | class Demo(models.Model): 102 | xxx = models.CharField() 103 | yyy = models.CharField() 104 | 105 | user = User.objects.get(id=123) 106 | print(user.demo.xxx) 107 | print(user.demo.yyy) 108 | ``` 109 | 110 | - 也可以使用 cached_property 对属性值进行缓存 111 | 112 | ```python 113 | from django.utils.functional import cached_property 114 | 115 | class User(models.Model): 116 | year = 1990 117 | month = 10 118 | day = 29 119 | 120 | @cached_property 121 | def age(self): 122 | today = datetime.date.today() 123 | birth_date = datetime.date(self.year, self.month, self.day) 124 | times = today - birth_date 125 | return times.days // 365 126 | ``` 127 | 128 | 129 | ## Django 中的 Form 表单验证 130 | 131 | - Django Form 核心功能:数据验证 132 | 133 | - 网页中 `
` 标签 134 | - `` 标签的 method 只能是 POST 或 GET 135 | - method=POST 时,表单数据在请求的 body 部分 136 | - method=GET 时, 表单数据会出现在 URL 里 137 | 138 | - Form 对象的属性和方法 139 | - `form.is_valid()` : 表单验证 140 | - `form.has_changed()` : 检查是否有修改 141 | - `form.clean_()` : 针对某字段进行特殊清洗和验证 142 | - `form.cleaned_data['fieldname']` : 清洗后的数据存放于这个属性 143 | 144 | - Form 的定义和使用 145 | 146 | ```python 147 | from django import forms 148 | 149 | class TestForm(forms.Form): 150 | TAGS = ( 151 | ('py', 'python'), 152 | ('ln', 'linux'), 153 | ('dj', 'django'), 154 | ) 155 | fid = forms.IntegerField() 156 | name = forms.CharField(max_length=10) 157 | tag = forms.ChoiceField(choices=TAGS) 158 | date = forms.DateField() 159 | 160 | POST = {'fid': 'bear', 161 | 'name': 'hello-1234567890', 162 | 'tag': 'django', 163 | 'date': '2017/12/17'} 164 | form = TestForm(POST) 165 | print(form.is_valid()) 166 | print(form.cleaned_data) # cleaned_data 属性是 is_valid 函数执行时动态添加的 167 | print(form.errors) 168 | ``` 169 | 170 | - ModelForm 可以通过相应的 Model 创建出 Form 171 | 172 | ```python 173 | class UserForm(ModelForm): 174 | class Meta: 175 | model = User 176 | fields = ['name', 'birth'] 177 | ``` 178 | 179 | 180 | ## 项目中的静态文件处理 181 | 182 | 1. Nginx 183 | 184 | Nginx 处理静态资源速度非常快, 并且自身还带有缓存. 185 | 186 | 但需要注意, 分布式部署的多台 Nginx 服务器上, 静态资源需要互相同步 187 | 188 | 2. CDN 189 | 190 | ![cdn](./img/CDN.png) 191 | 192 | CDN 的全称是 Content Delivery Network, 即内容分发网络. 193 | 194 | 它依靠部署在各地的边缘服务器, 通过中心平台的负载均衡、内容分发、调度等功能模块, 使用户就近获取所需内容, 降低网络拥塞, 提高用户访问响应速度和命中率. CDN 的关键技术主要有内容存储和分发技术. 195 | 196 | 3. 云存储 197 | 198 | - 常见的云存储有:亚马逊 S3 服务、阿里云的 OSS 、七牛云 等 199 | 200 | 4. 七牛云接入 201 | 202 | 1. 注册七牛云账号 203 | 204 | 2. 创建存储空间 205 | 206 | 3. 获取相关配置 207 | - AccessKey 208 | - SecretKey 209 | - Bucket_name 210 | - Bucket_URL 211 | 212 | 4. 安装 qiniu SDK:`pip install qiniu` 213 | 214 | 5. [根据接口文档进行接口封装](https://developer.qiniu.com/kodo/sdk/1242/python) 215 | 216 | 6. 按照需要将上传、下载接口封装成异步任务 217 | 218 | 7. 程序处理流程 219 | 220 | 1. 用户图片上传服务器 221 | 2. 服务器将图片上传到七牛云 222 | 3. 将七牛云返回的图片 URL 存入数据库 223 | 224 | 225 | ## Celery 及异步任务的处理 226 | 227 | 1. 模块组成 228 | 229 | ![celery](./img/celery.png) 230 | 231 | * 任务模块 Task 232 | 233 | 包含异步任务和定时任务. 其中, 异步任务通常在业务逻辑中被触发并发往任务队列, 而定时任务由 Celery Beat 进程周期性地将任务发往任务队列. 234 | 235 | * 消息中间件 Broker 236 | 237 | Broker, 即为任务调度队列, 接收任务生产者发来的消息(即任务), 将任务存入队列. Celery 本身不提供队列服务, 官方推荐使用 RabbitMQ 和 Redis 等. 238 | 239 | * 任务执行单元 Worker 240 | 241 | Worker 是执行任务的处理单元, 它实时监控消息队列, 获取队列中调度的任务, 并执行它. 242 | 243 | * 任务结果存储 Backend 244 | 245 | Backend 用于存储任务的执行结果, 以供查询. 同消息中间件一样, 存储也可使用 RabbitMQ, Redis 和 MongoDB 等. 246 | 247 | 2. 安装 248 | 249 | ``` 250 | pip install 'celery[redis]' 251 | ``` 252 | 253 | 3. 创建实例 254 | 255 | ```python 256 | import time 257 | from celery import Celery 258 | 259 | broker = 'redis://127.0.0.1:6379' 260 | backend = 'redis://127.0.0.1:6379/0' 261 | app = Celery('my_task', broker=broker, backend=backend) 262 | 263 | @app.task 264 | def add(x, y): 265 | time.sleep(5) # 模拟耗时操作 266 | return x + y 267 | ``` 268 | 269 | 4. 启动 Worker 270 | 271 | ``` 272 | celery worker -A tasks --loglevel=info 273 | ``` 274 | 275 | 5. 调用任务 276 | 277 | ```python 278 | from tasks import add 279 | 280 | add.delay(2, 8) 281 | ``` 282 | 283 | 6. 常规配置 284 | 285 | ```python 286 | broker_url = 'redis://127.0.0.1:6379/0' 287 | broker_pool_limit = 1000 # Borker 连接池, 默认是10 288 | 289 | timezone = 'Asia/Shanghai' 290 | accept_content = ['pickle', 'json'] 291 | 292 | task_serializer = 'pickle' 293 | result_expires = 3600 # 任务过期时间 294 | 295 | result_backend = 'redis://127.0.0.1:6379/0' 296 | result_serializer = 'pickle' 297 | result_cache_max = 10000 # 任务结果最大缓存数量 298 | 299 | worker_redirect_stdouts_level = 'INFO' 300 | ``` 301 | -------------------------------------------------------------------------------- /tutorial/swiper/doc/4.1.4-Social模块开发-1.md: -------------------------------------------------------------------------------- 1 | # Social 模块开发 1 2 | 3 | ## 功能概述 4 | 5 | 1. 交友模块 6 | - 获取推荐列表 7 | - 喜欢 / 超级喜欢 / 不喜欢 8 | - 反悔 (每天允许返回 3 次) 9 | - 查看喜欢过我的人 10 | 11 | 2. 好友模块 12 | - 查看好友列表 13 | - 查看好友信息 14 | 15 | 16 | ## 开发中的难点 17 | 18 | 1. 滑动需有大量用户,如何初始化大量用户以供测试? 19 | 2. 推荐算法 20 | 3. 如何从推荐列表中去除已经滑过的用户 21 | 4. 滑动操作,如何避免重复滑动同一人 22 | 5. 如果双方互相喜欢,需如何处理 23 | 6. 好友关系如何记录,数据库表结构如何设计? 24 | 7. 反悔接口 25 | 1. “反悔”都应该执行哪些操作 26 | 2. 每日只允许“反悔” 3 次应如何处理 27 | 3. 后期运营时,如何方便的修改反悔次数 28 | 8. 内部很深的逻辑错误如何比较方便的将错误码返回给最外层接口 29 | 30 | 31 | ## 关系分析 32 | 33 | 1. 滑动者与被滑动者 34 | - 一个人可以滑动很多人 35 | - 一个人可以被多人滑动 36 | - 结论: 同表之内构建起来的逻辑上的多对多关系 37 | 38 | 2. 用户与好友 39 | - 一个用户由多个好友 40 | - 一个用户也可以被多人加为好友 41 | - 结论: 同表之内构建起来的逻辑上的多对多关系, Friend 表实际上就是一个关系表 42 | 43 | 44 | ## 模型设计参考 45 | 46 | 1. Swiped (划过的记录) 47 | 48 | | Field | Description | 49 | | ----- | --------------- | 50 | | uid | 用户自身 id | 51 | | sid | 被滑的陌生人 id | 52 | | mark | 滑动类型 | 53 | | time | 滑动的时间 | 54 | 55 | 2. Friend (匹配到的好友) 56 | 57 | | Field | Description | 58 | | ----- | ----------- | 59 | | uid1 | 好友 ID | 60 | | uid2 | 好友 ID | 61 | 62 | 63 | ## 类方法与静态方法 64 | 65 | - `method` 66 | 67 | - 通过实例调用 68 | - 可以引用类内部的 **任何属性和方法** 69 | 70 | - `classmethod` 71 | 72 | - 无需实例化 73 | - 可以调用类属性和类方法 74 | - 无法取到普通的成员属性和方法 75 | 76 | - `staticmethod` 77 | 78 | - 无需实例化 79 | - **无法**取到类内部的任何属性和方法, 完全独立的一个方法 80 | 81 | 82 | ## 利用 Q 对象进行复杂查询 83 | 84 | ```python 85 | from django.db.models import Q 86 | 87 | # AND 88 | Model.objects.filter(Q(x=1) & Q(y=2)) 89 | 90 | # OR 91 | Model.objects.filter(Q(x=1) | Q(y=2)) 92 | 93 | # NOT 94 | Model.objects.filter(~Q(name='kitty')) 95 | ``` 96 | -------------------------------------------------------------------------------- /tutorial/swiper/doc/4.1.5-Social模块开发-2.md: -------------------------------------------------------------------------------- 1 | # Social 模块开发 2 2 | 3 | 同 4.1.4 4 | -------------------------------------------------------------------------------- /tutorial/swiper/doc/4.2.1-VIP模块开发及日志处理.md: -------------------------------------------------------------------------------- 1 | # VIP 模块开发及日志处理 2 | 3 | ## VIP、权限模块功能 4 | 5 | 1. VIP 分类 6 | - 非会员 7 | - 一级会员 8 | - 二级会员 9 | - 三级会员 10 | 11 | 2. 权限分类 12 | - 超级喜欢 13 | - 每日反悔 3 次 14 | - 查看喜欢过我的人 15 | 16 | 3. 权限分配 17 | - 非会员: 无任何权限 18 | - 一级会员: 超级喜欢 19 | - 二级会员: 超级喜欢 + 反悔3次 20 | - 三级会员: 超级喜欢 + 反悔3次 + 查看喜欢过我的人 21 | 22 | 23 | ## 开发难点 24 | 25 | 1. User 与 VIP 的关系 26 | 27 | - 一种 VIP 对应多个 User 28 | - 一个 User 只会有一种 VIP 29 | - 结论: 一对多关系 30 | 31 | 2. VIP 与权限 的关系 32 | - 一种 VIP 级别对应多种权限 33 | - 一个权限会属于在多种级别的 VIP 34 | - 结论: 多对多关系 35 | 36 | 3. 如何针对每个接口进行相应的权限检查 ? 37 | 38 | 39 | ## 模型设计 40 | 41 | 1. VIP (会员) 42 | 43 | | Field | Description | 44 | | ----- | ----------- | 45 | | name | 会员名称 | 46 | | level | 登记 | 47 | | price | 价格 | 48 | 49 | 2. Permission (权限) 50 | | Field | Description | 51 | | ----------- | ----------- | 52 | | name | 权限名称 | 53 | | description | 权限说明 | 54 | 55 | 56 | ## 日志相关功能 57 | 58 | 1. 开发统计函数,将每日登录数据统计到日志 59 | - 登录时间 60 | - 登录者的 uid 61 | 62 | 2. 使用 Linux 命令统计出每天的 DAU (日活跃) 63 | 64 | 65 | ## 日志处理 66 | 67 | 1. 日志的作用 68 | 69 | 1. 记录程序运行状态 70 | 1. 线上环境所有程序以 deamon 形式运行在后台, 无法使用 print 输出程序状态 71 | 2. 线上程序无人值守全天候运行, 需要有一种能持续记录程序运行状态的机制, 以便遇到问题后分析处理 72 | 2. 记录统计数据 73 | 3. 开发时进行 Debug (调试) 74 | 75 | 2. 基本用法 76 | 77 | ```python 78 | import logging 79 | 80 | # 设置日志格式 81 | fmt = '%(asctime)s %(levelname)7.7s %(funcName)s: %(message)s' 82 | formatter = logging.Formatter(fmt, datefmt="%Y-%m-%d %H:%M:%S") 83 | 84 | # 设置 handler 85 | handler = logging.handlers.TimedRotatingFileHandler('myapp.log', when='D', backupCount=30) 86 | handler.setFormatter(formatter) 87 | 88 | # 定义 logger 对象 89 | logger = logging.getLogger("MyApp") 90 | logger.addHandler(handler) 91 | logger.setLevel(logging.INFO) 92 | ``` 93 | 94 | 3. 日志的等级 95 | 96 | - DEBUG: 调试信息 97 | - INFO: 普通信息 98 | - WARNING: 警告 99 | - ERROR: 错误 100 | - FATAL: 致命错误 101 | 102 | 4. 对应函数 103 | 104 | - `logger.debug(msg)` 105 | - `logger.info(msg)` 106 | - `logger.warning(msg)` 107 | - `logger.error(msg)` 108 | - `logger.fatal(msg)` 109 | 110 | 5. 日志格式允许的字段 111 | 112 | - `%(name)s` : Logger 的名字 113 | - `%(levelno)s` : 数字形式的日志级别 114 | - `%(levelname)s` : 文本形式的日志级别 115 | - `%(pathname)s` : 调用日志输出函数的模块的完整路径名, 可能没有 116 | - `%(filename)s` : 调用日志输出函数的模块的文件名 117 | - `%(module)s` : 调用日志输出函数的模块名 118 | - `%(funcName)s` : 调用日志输出函数的函数名 119 | - `%(lineno)d` : 调用日志输出函数的语句所在的代码行 120 | - `%(created)f` : 当前时间, 用 UNIX 标准的表示时间的浮点数表示 121 | - `%(relativeCreated)d` : 输出日志信息时的, 自 Logger 创建以来的毫秒数 122 | - `%(asctime)s` : 字符串形式的当前时间。默认格式是“2003-07-08 16:49:45,896”。逗号后面的是毫秒 123 | - `%(thread)d` : 线程 ID。可能没有 124 | - `%(threadName)s` : 线程名。可能没有 125 | - `%(process)d` : 进程 ID。可能没有 126 | - `%(message)s` : 用户输出的消息 127 | 128 | 6. Django 中的日志配置 129 | 130 | ```python 131 | LOGGING = { 132 | 'version': 1, 133 | 'disable_existing_loggers': True, 134 | # 格式配置 135 | 'formatters': { 136 | 'simple': { 137 | 'format': '%(asctime)s %(module)s.%(funcName)s: %(message)s', 138 | 'datefmt': '%Y-%m-%d %H:%M:%S', 139 | }, 140 | 'verbose': { 141 | 'format': ('%(asctime)s %(levelname)s [%(process)d-%(threadName)s] ' 142 | '%(module)s.%(funcName)s line %(lineno)d: %(message)s'), 143 | 'datefmt': '%Y-%m-%d %H:%M:%S', 144 | } 145 | }, 146 | # Handler 配置 147 | 'handlers': { 148 | 'console': { 149 | 'class': 'logging.StreamHandler', 150 | 'level': 'DEBUG' if DEBUG else 'WARNING' 151 | }, 152 | 'info': { 153 | 'class': 'logging.handlers.TimedRotatingFileHandler', 154 | 'filename': f'{BASE_DIR}/logs/info.log', # 日志保存路径 155 | 'when': 'D', # 每天切割日志 156 | 'backupCount': 30, # 日志保留 30 天 157 | 'formatter': 'simple', 158 | 'level': 'INFO', 159 | }, 160 | 'error': { 161 | 'class': 'logging.handlers.TimedRotatingFileHandler', 162 | 'filename': f'{BASE_DIR}/logs/error.log', # 日志保存路径 163 | 'when': 'W0', # 每周一切割日志 164 | 'backupCount': 4, # 日志保留 4 周 165 | 'formatter': 'verbose', 166 | 'level': 'WARNING', 167 | } 168 | }, 169 | # Logger 配置 170 | 'loggers': { 171 | 'django': { 172 | 'handlers': ['console'], 173 | }, 174 | 'inf': { 175 | 'handlers': ['info'], 176 | 'propagate': True, 177 | 'level': 'INFO', 178 | }, 179 | 'err': { 180 | 'handlers': ['error'], 181 | 'propagate': True, 182 | 'level': 'WARNING', 183 | } 184 | } 185 | } 186 | ``` 187 | -------------------------------------------------------------------------------- /tutorial/swiper/doc/4.2.2-缓存及NoSQL的使用.md: -------------------------------------------------------------------------------- 1 | # 缓存及 NoSQL 的使用 2 | 3 | ## 开发任务 4 | 5 | 1. 为获取个人资料接口添加缓存处理 6 | 7 | 2. 统一为所有数据模型增加缓存处理 8 | - 任何 model 对象创建时,自动为该对象添加缓存 9 | - 任何 model 对象创建时,自动更新缓存数据 10 | 11 | 3. 开发全服人气排行功能 12 | 13 | - 被左滑 -5 分 14 | - 被右滑 +5 分 15 | - 被上滑 +7 分 16 | - 统计全服人气最高的 10 位用户 17 | 18 | 19 | ## 缓存处理 20 | 21 | 1. 缓存一般处理流程 22 | 23 | ```python 24 | data = get_from_cache(key) # 首先从缓存中获取数据 25 | if data is None: 26 | data = get_from_db() # 缓存中没有, 从数据库获取 27 | set_to_cache(key, data) # 将数据添加到缓存, 方便下次获取 28 | return data 29 | ``` 30 | 31 | 2. Django 的默认缓存接口 32 | 33 | ```python 34 | from django.core.cache import cache 35 | 36 | cache.set('a', 123, 10) 37 | a = cache.get('a') 38 | print(a) 39 | x = cache.incr(a) 40 | print(a) 41 | ``` 42 | 43 | 3. Django 中使用 Redis 缓存 44 | 45 | - 安装 django_redis: `pip install django_redis` 46 | 47 | - 配置 48 | 49 | ```Python 50 | # settings 添加如下配置 51 | CACHES = { 52 | "default": { 53 | "BACKEND": "django_redis.cache.RedisCache", 54 | "LOCATION": "redis://127.0.0.1:6379/1", 55 | "OPTIONS": { 56 | "CLIENT_CLASS": "django_redis.client.DefaultClient", 57 | "PICKLE_VERSION": -1, 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | ## Redis 的使用 64 | 65 | 1. [Redis 文档](http://redisdoc.com/) 66 | 67 | 2. 主要数据类型 68 | 69 | - **String 类**: 常用作普通缓存 70 | 71 | | CMD | Example | Description | 72 | |---------|---------------------------|-------------| 73 | | set | set('a', 123) | 设置值 | 74 | | get | get('a') | 获取值 | 75 | | incr | incr('a') | 自增 | 76 | | decr | decr('a') | 自减 | 77 | | mset | mset(a=123, b=456, c=789) | 设置多个值 | 78 | | mget | mget(['a', 'b', 'c']) | 获取多个值 | 79 | | setex | setex('kk', 21, 10) | 设置值的时候, 同时设置过期时间 | 80 | | setnx | setnx('a', 999) | 如果不存在, 则设置该值 | 81 | 82 | - **Hash 类**: 常用作对象存储 83 | 84 | | CMD | Example | Description | 85 | |---------|-----------------------------------|-------------| 86 | | hset | hset('obj', 'name', 'hello') | 在哈希表 obj 中添加一个 name = hello 的值 | 87 | | hget | hget('obj', 'name') | 获取哈希表 obj 中的值 | 88 | | hmset | hmset('obj', {'a': 1, 'b': 3}) | 在哈希表中设置多个值 | 89 | | hmget | hmget('obj', ['a', 'b', 'name']) | 获取多个哈希表中的值 | 90 | | hgetall | hgetall('obj') | 获取多个哈希表中所有的值 | 91 | | hincrby | hincrby('obj', 'count') | 将哈希表中的某个值自增 1 | 92 | | hdecrby | hdecrby('obj', 'count') | 将哈希表中的某个值自减 1 | 93 | 94 | - **List 类**: 常用作队列(消息队列、任务队列等) 95 | 96 | | CMD | Example | Description | 97 | |---------|-------------------------|------------------| 98 | | lpush | lpush(name, *values) | 向列表左侧添加多个元素 | 99 | | rpush | rpush(name, *values) | 向列表右侧添加多个元素 | 100 | | lpop | lpop(name) | 从列表左侧弹出一个元素 | 101 | | rpop | rpop(name) | 从列表右侧弹出一个元素 | 102 | | blpop | blpop(keys, timeout=0) | 从列表左侧弹出一个元素, 列表为空时阻塞 timeout 秒 | 103 | | brpop | brpop(keys, timeout=0) | 从列表右侧弹出一个元素, 列表为空时阻塞 timeout 秒 | 104 | | llen | llen(name) | 获取列表长度 | 105 | | ltrim | ltrim(name, start, end) | 从 start 到 end 位置截断列表 | 106 | 107 | - **Set 类**: 常用作去重 108 | 109 | | CMD | Example | Description | 110 | |-----------|------------------------|---------------| 111 | | sadd | sadd(name, *values) | 向集合中添加元素 | 112 | | sdiff | sdiff(keys, *args) | 多个集合做差集 | 113 | | sinter | sinter(keys, *args) | 多个集合取交集 | 114 | | sunion | sunion(keys, *args) | 多个集合取并集 | 115 | | sismember | sismember(name, value) | 元素 value 是否是集合 name 中的成员 | 116 | | smembers | smembers(name) | 集合 name 中的全部成员 | 117 | | spop | spop(name) | 随机弹出一个成员 | 118 | | srem | srem(name, *values) | 删除一个或多个成员 | 119 | 120 | - **SortedSet 类**: 常用作排行处理 121 | 122 | | CMD | Example | Description | 123 | |-----------|------------------------------------------|---------------| 124 | | zadd | zadd(name, a=12) | 添加一个 a, 值为 12 | 125 | | zcount | zcount(name, min, max) | 从 min 到 max 的元素个数 | 126 | | zincrby | zincrby(name, key, 1) | key 对应的值自增 1 | 127 | | zrange | zrange(name, 0, -1, withscores=False) | 按升序返回排名 0 到 最后一位的全部元素 | 128 | | zrevrange | zrevrange(name, 0, -1, withscores=False) | 按降序返回排名 0 到 最后一位的全部元素 | 129 | | zrem | zrem(name, *value) | 删除一个或多个元素 | 130 | 131 | 3. 使用 pickle 对 Redis 接口的封装 132 | 133 | ```python 134 | from pickle import dumps, loads 135 | 136 | rds = redis.Redis() 137 | 138 | def set(key, value): 139 | data = dumps(value) 140 | return rds.set(key, data) 141 | 142 | def get(key): 143 | data = rds.get(key) 144 | return loads(data) 145 | ``` 146 | 147 | 4. 动态修改 Python 属性和方法 148 | 149 | ```python 150 | class A: 151 | m = 128 152 | def __init__(self): 153 | self.x = 123 154 | 155 | def add(self, n): 156 | print(self.x + n) 157 | 158 | a = A() 159 | 160 | # 动态添加属性 (两种方式) 161 | a.y = 456 162 | setattr(a, 'z', 789) 163 | 164 | # 动态添加类属性 165 | A.y = 654 166 | 167 | # 类属性和实例属性互不影响 168 | print(A.y, a.y) 169 | 170 | # 动态添加实例方法 171 | def sub(self, n): 172 | print(self.x - n) 173 | A.sub = sub 174 | 175 | # 动态添加类方法 176 | @classmethod 177 | def mul(cls, n): 178 | print(cls.m * n) 179 | A.mul = mul 180 | 181 | # 动态添加静态方法 182 | @staticmethod 183 | def div(x, y): 184 | print(x / y) 185 | A.div = div 186 | 187 | # 属性修改的本质原因 188 | print(A.__dict__, a.__dict__) 189 | ``` 190 | 191 | 5. 在 Model 层插入缓存处理 192 | 193 | - Monkey Patch 也叫做 “猴子补丁”, 是一种编程技巧, 旨在运行时为对象动态添加、修改或者替换某项功能 194 | -------------------------------------------------------------------------------- /tutorial/swiper/doc/4.2.3-分布式数据库及性能评估.md: -------------------------------------------------------------------------------- 1 | # 分布式存储及 Web 服务器性能评估 2 | 3 | ## 分布式数据库 4 | 5 | 数据量不大时,单库单表即可支撑整个系统。当数据量达到一定规模后,则需通过分布式数据库支持。 6 | 同时,单点数据库无法保证服务高可用,一旦出现宕机整个服务便 “停摆” 了 7 | 8 | 常见的分布式数据库实现方式有 “分库” 和 “分表”, 也被称作 “数据分片” 9 | 10 | 1. 数据分片 11 | * 单表查询能力上限: 约为 500 万 左右 12 | * 方式: 分库、分表 13 | 14 | 2. 垂直拆分 15 | 16 | 单表字段太多的时候会进行垂直拆分, 不是为了分布式存储,而是为了提升单表性能 17 | 18 | ``` 19 | 垂直拆分 20 | | 21 | user | ext_info 22 | | 23 | | id name sex age location | uid aa bb cc dd ee ff | 24 | | ---------------------------- | -------------------------- | 25 | | 1 xxx f 11 beijing | 1 x x x x x x | 26 | | 2 xxx f 11 beijing | 2 x x x x x x | 27 | | 3 xxx f 11 beijing | 3 x x x x x x | 28 | | 4 xxx f 11 beijing | 4 x x x x x x | 29 | | 5 xxx f 11 beijing | 5 x x x x x x | 30 | | 6 xxx f 11 beijing | 6 x x x x x x | 31 | | 7 xxx f 11 beijing | 7 x x x x x x | 32 | | 8 xxx f 11 beijing | 8 x x x x x x | 33 | | 9 xxx f 11 beijing | 9 x x x x x x | 34 | ``` 35 | 36 | 3. 水平拆分 37 | 38 | 水平拆分既可以用在 “分表” 处理,也可用在 “分库” 处理 39 | 40 | ``` 41 | user 42 | id name sex age location aa bb cc dd ee ff 43 | ------------------------------------------------------ user_1 44 | 1 xxx f 11 beijing x x x x x x 45 | 2 xxx f 11 beijing x x x x x x 46 | 3 xxx f 11 beijing x x x x x x 47 | ------------------------------------------------------ user_2 48 | 4 xxx f 11 beijing x x x x x x 49 | 5 xxx f 11 beijing x x x x x x 50 | 6 xxx f 11 beijing x x x x x x 51 | ------------------------------------------------------ user_3 52 | 7 xxx f 11 beijing x x x x x x 53 | 8 xxx f 11 beijing x x x x x x 54 | 9 xxx f 11 beijing x x x x x x 55 | ``` 56 | 57 | * 按范围拆分 58 | * 优点: 构建简单, 扩容极其方便. 59 | * 缺点: 不能随运营发展均衡分配资源 60 | * 示例 61 | 62 | ``` 63 | Database-1 1 - 500W <- uid: 3120482 64 | Database-2 500W - 1000W 65 | Database-3 1000W - 1500W <- post_id: 20278327 66 | Database-4 1500W - 2000W 67 | ``` 68 | 69 | * 按余数拆分 70 | * 优点: 能够随着运营发展均匀分配负载 71 | * 缺点: 扩容不方便, 前期投入大 72 | * 示例 73 | 74 | ``` 75 | uid = 3120483 76 | mod = uid % len(Databases) -> 3 77 | db_name = 'Database-3' 78 | 79 | Database-0 10 20 30 ... 3120480 80 | Database-1 1 11 21 31 ... 3120481 81 | Database-2 2 12 22 32 ... 3120482 82 | Database-3 3 13 23 33 ... 3120483 83 | Database-4 4 14 24 34 ... 3120484 84 | Database-5 5 15 25 35 ... 3120485 85 | Database-6 6 16 26 36 ... 3120486 86 | Database-7 7 17 27 37 ... 3120487 87 | Database-8 8 18 28 38 ... 3120488 88 | Database-9 9 19 29 39 ... 3120489 89 | ``` 90 | 91 | 4. 分布式数据库的 ID 92 | - 必须保证全服多机上产生的 ID 唯一 93 | - 常见全局唯一 ID 生成策略 94 | 1. 基于存储的自增 ID 95 | - 可在 Redis 中为每一个表记录当前最新 ID 是多少, 获取下一个 ID 时进行自增 96 | - 优点: 思路简单, ID 连续 97 | - 缺点: 有存储依赖, 一旦 Redis 出现问题, 则会影响全部数据库存储 98 | 2. 基于算法确保唯一 99 | - 常见算法有 UUID、COMB、Snowflake、ObjectID 等 100 | - 优点: 快速、无存储依赖 101 | - 缺点: 一般产生的 ID 数值都比较大, 某些算法的 ID 并非是增序 102 | 103 | ## 数据库集群 104 | 105 | * 通过增加冗余的数据库服务器可构建数据库集群 106 | * 同一集群中的多台数据库保存的数据必须完全一致 107 | * 集群中一台服务器宕机,其他服务器可以继续提供服务 108 | * 常见结构:一主多从,主从之间通过 binlog 进行数据同步 109 | * 读写分离 110 | * 主机用来做数据写入;从机用来数据读取 111 | * 程序自身实现读写分离 112 | 113 | ![self](./img/master-slave-1.png) 114 | 115 | * 使用第三方代理实现读写分离 116 | 117 | ![proxy](./img/master-slave-2.png) 118 | 119 | ## 服务高可用 120 | 121 | 1. 对软硬件的冗余, 以消除单点故障. 任何系统都会有一个或多个冗余系统做备份 122 | 2. 对故障的检测和恢复. 检测故障以及用备份的结点接管故障点, 也就是 “故障转移” 123 | 3. 需要很可靠的交汇点 (CrossOver). 这是一些不容易冗余的结点, 比如域名解析, 负载均衡器等. 124 | 125 | 126 | ## 并发与性能 127 | 128 | * 概念 129 | * 理解 I/O 的概念 130 | * 理解 “同步/异步”、“阻塞/非阻塞” 131 | * 了解 “事件驱动” 和 “多路复用” 132 | * 异步模型并不会消灭阻塞,而是在发生 I/O 阻塞时切换到其他任务,从而达到异步非阻塞 133 | 134 | * 计算密集型 135 | * CPU 长时间满负荷运行, 如图像处理、大数据运算、科学运算等 136 | * 计算密集型: 用 C 语言或 Cython 补充 137 | 138 | * I/O 密集型 139 | * 网络 IO, 文件 IO, 设备 IO 等 140 | * Unix: 一切皆文件 141 | 142 | * 多任务处理 143 | * 进程、线程、协程调度的过程叫做上下文切换 144 | * 进程、线程、协程对比 145 | 146 | 名称 | 资源占用 | 数据通信 | 上下文切换 (Context) 147 | -----|---------|------------------------------|------------------ 148 | 进程 | 大 | 不方便 (网络、共享内存、管道等) | 操作系统按时间片切换, 不够灵活, 慢 149 | 线程 | 小 | 非常方便 | 按时间片切换, 不够灵活, 快 150 | 协程 | 非常小 | 非常方便 | 根据I/O事件切换, 更加有效的利用 CPU 151 | 152 | * 全局解释器锁 ( GIL ) 153 | 154 | ![GIL](./img/GIL.png) 155 | 156 | * 它确保任何时候一个进程中都只有一个 Python 线程能进入 CPU 执行。 157 | * 全局解释器锁造成单个进程无法使用多个 CPU 核心 158 | * 通过多进程来利用多个 CPU 核心,一般进程数与CPU核心数相等,或者CPU核心数两倍 159 | 160 | * 协程 161 | 162 | - Python 下协程的发展: 163 | - stackless / greenlet / gevent 164 | - tornado 通过纯 Python 代码实现了协程处理 (底层使用 yield) 165 | - asyncio: Python 官方实现的协程 166 | 167 | - asyncio 实现协程 168 | 169 | ```python 170 | import asyncio 171 | 172 | async def foo(n): 173 | for i in range(10): 174 | print('wait %s s' % n) 175 | await asyncio.sleep(n) 176 | return i 177 | 178 | task1 = foo(1) 179 | task2 = foo(1.5) 180 | tasks = [asyncio.ensure_future(task1), 181 | asyncio.ensure_future(task2)] 182 | 183 | loop = asyncio.get_event_loop() # 事件循环,协程调度器 184 | loop.run_until_complete( asyncio.wait(tasks) ) 185 | ``` 186 | 187 | * 结论:通常使用多进程 + 多协程达到最大并发性能 188 | * 因为 GIL 的原因, Python 需要通过多进程来利用多个核心 189 | * 线程切换效率低, 而且应对 I/O 不够灵活 190 | * 协程更轻量级,完全没有协程切换的消耗,而且可以由程序自身统一调度和切换 191 | * HTTP Server 中,每一个请求都由独立的协程来处理 192 | 193 | * 单台服务器最大连接数 194 | * 文件描述符: 限制文件打开数量 (一切皆文件) 195 | * 内核限制: `net.core.somaxconn` 196 | * 内存限制 197 | * 修改文件描述符: `ulimit -n 65535` 198 | 199 | * 使用 Gunicorn 驱动 Django 200 | * 201 | * Gunicorn 扮演 HTTPServer 的角色 202 | * HTTPServer: 只负责网络连接 (TCP握手、数据收/发) 203 | 204 | * 分清几个概念 205 | * WSGI: 206 | 全称是 WebServerGatewayInterface, 它是 Python 官方定义的一种描述 HTTP 服务器 (如nginx)与 Web 应用程序 (如 Django、Flask) 通信的规范。全文定义在 [PEP333](https://www.python.org/dev/peps/pep-0333/) 207 | 208 | * uwsgi: 209 | 与 WSGI 类似, 是 uWSGI 服务器自定义的通信协议, 用于定义传输信息的类型(type of information)。每一个 uwsgi packet 前 4byte 为传输信息类型的描述, 与 WSGI 协议是两种东西, 该协议性能远好于早期的 Fast-CGI 协议。 210 | 211 | * uWSGI: 212 | uWSGI 是一个全功能的 HTTP 服务器, 实现了WSGI协议、uwsgi 协议、http 协议等。它要做的就是把 HTTP协议转化成语言支持的网络协议。比如把 HTTP 协议转化成 WSGI 协议, 让 Python 可以直接使用。 213 | 214 | ``` 215 | HTTP Server => 负责 1. 接受、断开客户端请求; 2. 接收、发送网络数据 216 | ^ 217 | | 218 | v 219 | WSGI => 负责 在 HTTPServer 和 WebApp 之间进行数据转换 220 | ^ 221 | | 222 | v 223 | Web App => 负责 Web 应用的业务逻辑 224 | ``` 225 | 226 | 227 | ## 压力测试 228 | 229 | * 常用工具 230 | - [ab (apache benchmark)](https://httpd.apache.org/docs/2.4/programs/ab.html) 231 | - [siege](https://github.com/JoeDog/siege) 232 | - webbench 233 | - [wrk](https://github.com/wg/wrk) 234 | 235 | * Web 系统性能关键指标: **RPS** (Requests per second) 236 | * 其他: 237 | * QPS (每秒查询数) 238 | * TPS (每秒事务数, 数据库指标) 239 | 240 | * Ubuntu 下安装 ab: `apt-get install apache2-utils` 241 | * 压测: `ab -k -n 1000 -c 300 http://127.0.0.1:9000/` 242 | -------------------------------------------------------------------------------- /tutorial/swiper/doc/4.2.4-上线部署及脚本开发.md: -------------------------------------------------------------------------------- 1 | # 上线部署与 Shell 脚本开发 2 | 3 | ## 服务器环境部署 4 | 5 | ### Step-1: 创建登录密钥 6 | 7 | ```bash 8 | $ ssh-keygen -t rsa # 执行此命令 9 | 10 | # 程序输出 11 | Generating public/private rsa key pair. 12 | Enter file in which to save the key (/root/.ssh/id_rsa): # 确认密钥文件位置 (敲回车) 13 | Enter passphrase (empty for no passphrase): # 为密钥设置密码 (无需密码, 直接回车) 14 | Enter same passphrase again: # 确认密码 (再次回车) 15 | Your identification has been saved in /root/.ssh/id_rsa. 16 | Your public key has been saved in /root/.ssh/id_rsa.pub. 17 | The key fingerprint is: 18 | SHA256:WpGZKdaT3SbGlx+pNi6H5/SBNZXjk5C468y+4MeNKxs root@box 19 | The key's randomart image is: 20 | +---[RSA 2048]----+ 21 | | | 22 | | . O ......| 23 | | o X =.=ooo.| 24 | | . . + +.oooo| 25 | | S .+ ++ | 26 | | o +.+ ..| 27 | | . E+.O . | 28 | | ..*X o . | 29 | | o=B+ . | 30 | +----[SHA256]-----+ 31 | ``` 32 | 33 | ### Step-2: 复制公钥到远程服务器 34 | 1. 本地打开 `~/.ssh/id_rsa.pub` 文件, 复制全部文本内容 35 | 2. ssh 登录到远程服务器, vim 打开 `~/.ssh/authorized_keys` 文件 36 | 3. 将复制的内容写入文件, 保存退出 37 | 38 | ### Step-3: 关闭密码登录 39 | 40 | 密钥设置好以后,便可以关闭服务器的密码登录,保证服务器不会被暴力破解,增强安全性. 41 | 42 | 以后登录服务器只允许通过密钥登录。 43 | 44 | 1. 登录服务器, 打开 `/etc/ssh/sshd_config` 文件 45 | 2. 找到 `PasswordAuthentication yes` 这行设置,将 `yes` 改为 `no` 46 | 3. 执行 `service ssh restart` 重启 SSH 服务 47 | 48 | 49 | ### Step-4: 更新服务器软件,安装所需组件 50 | 51 | ```bash 52 | $ sudo apt update -y 53 | $ sudo apt upgrade -y 54 | # 安装软件包 55 | $ sudo apt install -y gcc make openssl git mysql-server zip p7zip apache2-utils sendmail 56 | # 安装必要依赖 57 | $ sudo apt install -y libbz2-dev libpcre3 libpcre3-dev libreadline-dev libsqlite3-dev libssl-dev zlib1g-dev 58 | ``` 59 | 60 | ### Step-5: 安装 Nginx、Redis 61 | 62 | 1. 浏览器中打开 nginx、redis 官网,找到其最新稳定版安装包的下载地址,右键点击复制 63 | 2. ssh 登录到服务器 64 | 3. 通过 wget 下载复制的软件包地址 65 | 4. 解压、编译、安装 66 | 67 | ```bash 68 | $ cd nginx-1.14.2 69 | $ ./configure 70 | $ make 71 | $ make install 72 | ``` 73 | 74 | ### Step-6: 配置 nginx、redis、mysql 等组件 75 | ### Step-7: 运行各组件 76 | ### Step-8: 代码上传、运行 77 | 78 | 1. 登录服务器, 创建好项目保存路径 79 | 80 | ```bash 81 | $ cd /opt/ 82 | $ mkdir -p swiper/logs 83 | ``` 84 | 85 | 2. 进入项目目录,执行如下操作 86 | 87 | ```bash 88 | $ rsync -crvP --exclude={.venv,.git,__pycache__,logs} ./ root@X.X.X.X:/opt/swiper/ 89 | ``` 90 | 91 | 3. 运行 92 | 93 | ```bash 94 | $ gunicorn -c swiper/gunicorn-config.py swiper.wsgi 95 | ``` 96 | 97 | 98 | ## Shell 脚本编程 99 | 100 | ### 首行 101 | 102 | 脚本文件第一行通过注释的方式指明执行脚本的程序 103 | 104 | 常见方式有 `#!/bin/bash` 或 `#!/usr/bin/env bash` 105 | 106 | 107 | ### 变量 108 | 109 | ```bash 110 | # 变量定义: 等号前后没有空格 111 | a=12345678 112 | 113 | # 使用变量: 变量名前面加上 $ 符 114 | echo "----$a----" 115 | printf "===>$a<===\n" 116 | 117 | # 定义当前Shell下的全局变量 118 | export ABC=9876543210123456789 119 | 120 | # 定义完后, 在终端里用 source 加载脚本 121 | source ./test.sh 122 | ``` 123 | 124 | 125 | ### 常用的系统环境变量 126 | 127 | `$PATH`: 可执行文件目录 128 | `$PWD`: 当前目录 129 | `$HOME`: 家目录 130 | 131 | 132 | ### 分支控制语句: `if` 133 | 134 | ```bash 135 | if [[ $a == "12345678" ]]; then 136 | echo 'this is a arg' 137 | elif [ -d $0 ]; then 138 | echo 'this is a dir' 139 | elif [ -f $0 ]; then 140 | echo 'this is a file' 141 | else 142 | echo '98765432' 143 | fi 144 | ``` 145 | 146 | 147 | ### 循环控制语句: for 148 | 149 | ```bash 150 | # 从1到10显示数字 151 | for i in $(seq 1 10) 152 | do 153 | echo "num: $i" 154 | done 155 | ``` 156 | 157 | 158 | ### 函数 159 | 160 | ```bash 161 | foo() { 162 | echo "Hello BJ-1813" 163 | for f in `ls ../` 164 | do 165 | echo $f 166 | done 167 | } 168 | 169 | # 函数的使用,不需要小括号 170 | foo 171 | ``` 172 | 173 | 174 | ### 函数中使用参数 175 | 176 | ```bash 177 | bar() { 178 | echo "执行者是 $0" 179 | echo "参数数量是 $#" 180 | 181 | if [ -d $1 ]; then # 检查传入的第一个参数是否是文件夹 182 | for f in `ls $1` 183 | do 184 | echo $f 185 | done 186 | elif [ -f $1 ]; then 187 | echo 'This is a file: $1' # 单引号内的变量不会被识别 188 | echo "This is a file: $1" # 如果不是文件夹,直接显示文件名 189 | else 190 | echo 'not valid' # 前面都不匹配显示默认语句 191 | fi 192 | } 193 | ``` 194 | 195 | 196 | ## 开发服务器部署脚本 197 | 198 | * 系统部署脚本 199 | * 将前述步骤通过脚本方式组织起来 200 | * 可通过参数方式,选择执行独立步骤 201 | 202 | * 代码发布脚本 203 | * 上传脚本到服务器 204 | * 上传完成后,重启服务器 205 | 206 | * 程序启动脚本 207 | 208 | * 程序停止脚本 209 | 210 | * 程序重启脚本 211 | * 重启过程服务不可中断 212 | * 不间断重启: `kill -HUP [进程 ID]` 213 | * 大型分布式服务器不间断重启: 214 | - 禁止一次性重启全部机器 215 | - 一次重启集群中的一部分 216 | - 重启后的服务器没有问题后,再重启第二部分 217 | - 依次重复上述步骤,直至全部重启完成 218 | -------------------------------------------------------------------------------- /tutorial/swiper/doc/4.2.5-服务器架构.md: -------------------------------------------------------------------------------- 1 | # 反向代理、负载均衡、服务器架构 2 | 3 | ## Nginx 与负载均衡 4 | 5 | * 反向代理 6 | 7 | 1. 将用户请求转发给内部服务器,保护内网拓扑结构 8 | 9 | ``` 10 | / Django-1 11 | user -> proxy ── Django-2 12 | \ Django-3 13 | ``` 14 | 15 | 2. 可以解析用户请求,代理静态文件 16 | 17 | * 负载均衡 18 | - 轮询: rr (默认) 19 | - 权重: weight 20 | - IP哈希: ip_hash 21 | - 最小连接数: least_conn 22 | 23 | * 其他负载均衡 24 | * F5: 硬件负载均衡设备, 性能最好, 价格昂贵 25 | * LVS: 工作在 2层 到 4层 的专业负载均衡软件, 只有 3 种负载均衡方式, 配置简单 26 | * HAProxy: 工作在 4层 到 7层 的专业负载均衡软件, 支持的负载均衡算法丰富 27 | * 性能比较: F5 > LVS > HAProxy > Nginx 28 | 29 | * LVS 的优势 30 | 31 | - 常规负载均衡 32 | 33 | 进出都要经过负载均衡服务器. 响应报文较大, 面对大量请求时负载均衡节点本身可能会成为瓶颈 34 | 35 | ``` 36 | 发送请求: User -> LoadBalancer -> Server 37 | 接收响应: User <- LoadBalancer <- Server 38 | ``` 39 | 40 | - LVS DR 模式 41 | 42 | LoadBalancer 与 Server 同在一个网段, 共享同一个公网 IP, 响应报文可以由 Server 直达 User 43 | 44 | ``` 45 | 发送请求: User -> LoadBalancer -> Server 46 | 接收响应: User <───────────────── Server 47 | ``` 48 | 49 | * 可以不使用 Nginx, 直接用 gunicorn 吗? 50 | * Nginx 相对于 Gunicorn 来说更安全 51 | * Nginx 可以用作负载均衡. 52 | 53 | * 处理静态文件相关配置 54 | 55 | ```nginx 56 | location /statics/ { 57 | root /project/bbs/; 58 | expires 30d; 59 | access_log off; 60 | } 61 | 62 | location /medias/ { 63 | root /project/bbs/; 64 | expires 30d; 65 | access_log off; 66 | } 67 | ``` 68 |
69 | 70 | 71 | ## 服务器架构 72 | 73 | 1. 架构研究的 5 个方面 74 | * 高性能 75 | * 高可用 76 | * 可伸缩 77 | * 可扩展 78 | * 安全性 79 | 80 | 2. 简单、实用的服务器架构图 81 | 82 | * 分层结构: 功能模块解耦合 83 | * 每层多台机器: 有效避免单点故障 84 | * 每层均可扩容: 能通过简单的方式提升服务器的性能、可用性、扛并发能力 85 | 86 | ``` 87 | User Request cli_ip(12.23.34.45) -> ip_hash: 3 88 | | | | | 89 | V V V V 90 | www.example.com ---> 第一层负载均衡 91 | DNS 轮询 92 | / \ 93 | V V 94 | Nginx Nginx 95 | 115.2.3.11 115.2.3.12 ---> Nginx 绑定公网 IP 96 | / | \ / | \ 97 | / | X | \ 98 | V V V V V V 99 | AppServer AppServer AppServer AppServer ---> Gunicorn + Django 100 | 10.0.0.1 10.0.0.2 10.0.0.3 10.0.0.4 ---> AppServer 绑定内网 IP 101 | weight:10 weight:20 weight:20 weight:20 ---> 权重 102 | | | | | 103 | V V V V 104 | +------------------------------------------+ 105 | | 缓存层 主机 <--> 从机 | 106 | +------------------------------------------+ 107 | | | | | 108 | V V V V 109 | +------------------------------------------+ 110 | | 数据库 主机 <--> 从机 | 111 | +------------------------------------------+ 112 | ``` 113 |
114 | 115 | 116 | ## 服务器架构的发展 117 | 118 | * 早期服务器, 所有服务在一台机器 119 | 120 | ![arch-1](./img/arch-1.jpg) 121 | 122 |
123 | 124 | * 服务拆分, 应用、数据、文件等服务分开部署 125 | 126 | ![arch-2](./img/arch-2.jpg) 127 | 128 |
129 | 130 | * 利用缓存提升性能 131 | 132 | ![arch-3](./img/arch-3.jpg) 133 | 134 |
135 | 136 | * 应用服务器分布式部署, 提升网站并发量和吞吐量 137 | 138 | ![arch-4](./img/arch-4.jpg) 139 | 140 |
141 | 142 | * 通过读写分离读写分离提升数据库性能和数据可靠性 143 | 144 | ![arch-5](./img/arch-5.jpg) 145 | 146 |
147 | 148 | * 使用反向代理、CDN、云存储等技术提升静态资源访问速度, 并能有效提升不同地域的访问体验 149 | 150 | ![arch-6](./img/arch-6.jpg) 151 | 152 |
153 | 154 | * 通过分布式数据库和分布式文件系统满足数据和文件海量存储需求, 并进一步提升数据可靠性 155 | 156 | ![arch-7](./img/arch-7.jpg) 157 | 158 |
159 | 160 | * 增加搜索引擎 和 NoSQL 161 | 162 | ![arch-8](./img/arch-8.jpg) 163 | 164 |
165 | 166 | * 增加消息队列服务器, 让请求处理异步化 167 | 168 | ![arch-9](./img/arch-9.jpg) 169 | 170 |
171 | 172 | * 拆分应用服务器与内部服务 173 | 174 | ![arch-10](./img/arch-10.jpg) 175 | 176 |
177 | 178 | 179 | ## 其他 180 | 181 | - 服务器性能预估 182 | 183 | 1. 首先需知道网站日活跃 (DAU) 数据 184 | 2. 按每个活跃用户产生 100 个请求计算出 “每日总请求量” 185 | 186 | 不同类型的网站请求量差异会很大, 可以自行调整一个用户产生的请求数 187 | 188 | ``` 189 | 每日总请求量 = DAU x 单个用户请求量 190 | ``` 191 | 192 | 3. 有了总请求量便可计算 “每日峰值流量”, 流量一般单位为 rps (requests per second) 193 | 194 | 根据经验可知: 每天 80% 的请求会在 20% 的时间内到达 195 | 196 | 由此可知: 197 | 198 | ``` 199 | 每日总请求量 x 80% 200 | 每日峰值流量 = ─────────────────────── 201 | 86400 x 20% 202 | ``` 203 | 204 | 4. 一般带负载的 web 服务器吞吐量约为 300rps, 所以: 205 | 206 | ``` 207 | WebServer 数量 = 每日峰值流量 / 300 208 | ``` 209 | 210 | 5. 得到 WebServer 数量以后, 再根据用户规模和请求量估算 Nginx、Cache、Database 等服务器的数量 211 | 212 | - 真实工作中服务器分配情况 213 | -------------------------------------------------------------------------------- /tutorial/swiper/doc/chat_api.md: -------------------------------------------------------------------------------- 1 | WebSocket 通信协议 2 | ================== 3 | 4 | ## 统一数据包格式 5 | 6 | 数据包整体为一个二元 json 列表, 第一位为包的标识, 字符串类型, 第二位是具体数据 7 | 如 '发送私聊消息': 8 | ```python 9 | [ 10 | 'PRIVATE', # tag 11 | {'to': '112233', 'msg': 'abcdefg'} # data 12 | ] 13 | ``` 14 | 15 | ## 协议详情 16 | 17 | ### 1. 私人聊天 18 | 19 | * **tag**: PRIVATE 20 | * **data**: 消息体,分为发送包和接收包两种状况 21 | * **示例**: 22 | - 消息发送方: 23 | ```python 24 | [ 25 | 'PRIVATE', # tag 26 | { 27 | 'to': '12345', # 对方的 uid 28 | 'msg': 'ba la ba la', # 消息内容 29 | } 30 | ] 31 | 32 | - 消息接收方: 33 | ```python 34 | [ 35 | 'PRIVATE', # tag 36 | { 37 | 'tm': 1437125953, # 时间戳 38 | 'from': '67890', # 发送者 uid 39 | 'nickname': 'Bob', # 昵称 40 | 'avatar': 'http://abc.cn/xxx.png', # 头像 URL 41 | 'msg': 'ba la ba la' # 消息内容 42 | } 43 | ] 44 | ``` 45 | 46 | ### 2. 离线时聊天记录 47 | 48 | 服务器会在用户上线后,自动推送离线消息 49 | 50 | * **tag**: HISTORY 51 | * **data**: 消息列表, list 类型, 嵌套每一个消息体 52 | * **示例**: 53 | 54 | ```python 55 | [ 56 | "HISTORY", # tag 57 | [ 58 | {...}, # 离线消息 1 59 | {...}, # 离线消息 2 60 | {...}, # 离线消息 3 61 | ] 62 | ] 63 | ``` 64 | 65 | ### 3. 系统广播推送 66 | 67 | * **tag**: BROADCAST 68 | * **data**: 广播内容, str 类型 69 | * **示例**: 70 | 71 | ```python 72 | [ 73 | 'BROADCAST', # tag 74 | '系统公告:吧啦吧啦吧啦。。。' 75 | ] 76 | ``` 77 | 78 | ### 4. 异常 79 | 80 | * **tag**: ERR 81 | * **data**: 错误描述, str 类型 82 | * **示例**: 83 | 84 | ```python 85 | [ 86 | 'ERR', 87 | 'DataError' 88 | ] 89 | ``` 90 | -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/CDN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/CDN.png -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/GIL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/GIL.png -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/arch-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/arch-1.jpg -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/arch-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/arch-10.jpg -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/arch-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/arch-2.jpg -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/arch-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/arch-3.jpg -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/arch-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/arch-4.jpg -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/arch-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/arch-5.jpg -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/arch-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/arch-6.jpg -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/arch-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/arch-7.jpg -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/arch-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/arch-8.jpg -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/arch-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/arch-9.jpg -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/celery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/celery.png -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/fb-dev.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/fb-dev.jpg -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/front-back.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/front-back.jpg -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/git.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/git.png -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/master-slave-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/master-slave-1.png -------------------------------------------------------------------------------- /tutorial/swiper/doc/img/master-slave-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/doc/img/master-slave-2.png -------------------------------------------------------------------------------- /tutorial/swiper/doc/web_api.md: -------------------------------------------------------------------------------- 1 | 前后端接口 2 | ========== 3 | 4 | ## 接口格式 5 | 6 | 1. 所有获取数据的 HTTP 接口, 参数均由 GET 方法传递 7 | 2. 所有修改数据的 HTTP 接口, 参数均由 POST 方法传递 8 | 3. 返回值均为 JSON 格式, 必须包含两个字段: "code", "data" 9 | 1. code 字段为状态码(status code), int 类型, 为 0 时表示返回值正常, 其他任何值时均表示异常, 需客户端根据状态码为用户提供不同的错误提示 10 | 2. data 字段为返回的数据, 字典类型. 详情见下面具体接口. 当一个接口仅需状态码确认时, data 可以为空 11 | 3. 示例 12 | ```json 13 | { 14 | "code": 0, 15 | "data": { 16 | "user": { 17 | "uid": 123321, 18 | "username": "Lion", 19 | "age": 21, 20 | "sex": "Male" 21 | }, 22 | "date": "2018-09-12", 23 | } 24 | } 25 | ``` 26 | 27 | ## status 状态码 28 | 29 | code | description 30 | -----|------------- 31 | 0 | 正常 32 | 1000 | 服务器内部错误 33 | 1001 | 参数错误 34 | 1002 | 数据错误 35 | 1003 | 不存在 36 | 1004 | 达到上限 37 | 1005 | 没有权限 38 | 1006 | 超时 39 | 1007 | 已过期 40 | 1008 | 时间未到 41 | 1009 | 无效验证码 42 | 2000 | 用户未登录 43 | 2001 | 名字冲突 44 | 2002 | 金钱不足 45 | 2003 | 用户不存在 46 | 2004 | 不是好友关系 47 | 48 | 49 | ## 基础数据格式 50 | 51 | 1. User 52 | ```json 53 | { 54 | "uid": 123, // 用户 id 55 | "nickname": "Miao", // 用户名 56 | "age": 21, // 年龄 57 | "sex": "M", // 性别 58 | "location": "China/Beijing", // 常居地 59 | "avatars": [ // 头像 URL 列表, 最多为 6 张 60 | "http://xxx.com/user/avatar/123/1.jpg", 61 | "http://xxx.com/user/avatar/123/2.jpg", 62 | "http://xxx.com/user/avatar/123/3.jpg", 63 | ... 64 | ] 65 | } 66 | ``` 67 | 68 | ## User 接口 69 | 70 | 1. 提交手机号 71 | * **Description**: 提交手机号,根据结果判断下一步需要提交的数据 72 | * **Method**: POST 73 | * **Path**: /user/verify 74 | * **Params**: 75 | 76 | field | required | type | description 77 | ------|----------|------|----------------------- 78 | phone | Yes | str | 手机号, "18888888888" 79 | 80 | * **Return**: 81 | 82 | data 为 null 83 | 84 | ```json 85 | { 86 | "code": 0, 87 | "data": null 88 | } 89 | ``` 90 | 91 | 2. 提交验证码登录 92 | * **Description**: 根据上一步的结果提交需要的数据 93 | * **Method**: POST 94 | * **Path**: /user/login 95 | * **Params**: 96 | 97 | field | required | type | description 98 | ------|----------|------|----------------------- 99 | phone | Yes | int | 手机号 100 | code | Yes | int | 验证码 101 | 102 | * **Return**: 103 | 104 | field | required | type | description 105 | ------|----------|------|----------------------- 106 | user | Yes | User | 用户数据 107 | 108 | 示例: 109 | ```json 110 | { 111 | "code": 0, 112 | "data": { 113 | "user": { 114 | "uid": 123, // 用户 id 115 | "nickname": "Miao", // 用户名 116 | "age": 21, // 年龄 117 | "sex": "M", // 性别 118 | "location": "China/Beijing", // 常居地 119 | "avatars": [ // 头像 URL 列表, 最多为 6 张 120 | "http://xxx.com/user/avatar/123/1.jpg", 121 | "http://xxx.com/user/avatar/123/2.jpg", 122 | "http://xxx.com/user/avatar/123/3.jpg", 123 | ... 124 | ] 125 | }, 126 | }, 127 | } 128 | ``` 129 | 130 | 3. 获取配置信息 131 | * **Description**: - 132 | * **Method**: GET 133 | * **Path**: /user/profile/show 134 | * **Params**: 无需参数 135 | 136 | * **Return**: 137 | 138 | field | required | type | description 139 | ---------------|----------|-------|----------------------- 140 | location | Yes | str | 目标城市 141 | min_distance | Yes | float | 最小查找范围 142 | max_distance | Yes | float | 最大查找范围 143 | min_dating_age | Yes | int | 最小交友年龄 144 | max_dating_age | Yes | int | 最大交友年龄 145 | dating_sex | Yes | str | 匹配的性别 146 | vibration | Yes | bool | 开启震动 147 | only_matche | Yes | bool | 不让为匹配的人看我的相册 148 | auto_play | Yes | bool | 自动播放视频 149 | 150 | 4. 修改配置 151 | * **Description**: 需要修改哪个就传哪个, 虽然参数均为非必选参数, 但至少传一个 152 | * **Method**: POST 153 | * **Path**: /user/profile/update 154 | * **Params**: 155 | 156 | field | required | type | description 157 | ---------------|----------|-------|----------------------- 158 | location | No | str | 目标城市 159 | min_distance | No | float | 最小查找范围 160 | max_distance | No | float | 最大查找范围 161 | min_dating_age | No | int | 最小交友年龄 162 | max_dating_age | No | int | 最大交友年龄 163 | dating_sex | No | str | 匹配的性别 164 | vibration | No | bool | 开启震动 165 | only_matche | No | bool | 不让为匹配的人看我的相册 166 | auto_play | No | bool | 自动播放视频 167 | 168 | * **Return**: 169 | 170 | data 为 null 171 | 172 | 5. 上传头像 173 | * **Description**: 至少上传一张 174 | * **Method**: POST 175 | * **Path**: /user/avatar/upload 176 | * **Params**: 177 | 178 | field | required | type | description 179 | -------|----------|------|----------------------- 180 | first | No | str | 第一张 181 | second | No | str | 第二张 182 | third | No | str | 第三张 183 | fourth | No | str | 第四张 184 | fifth | No | str | 第五张 185 | sixth | No | str | 第六张 186 | 187 | * **Return**: 188 | 189 | data 为 null 190 | 191 | 192 | ## Social 接口 193 | 194 | 1. 获取推荐用户 195 | * **Description**: 196 | * **Method**: GET 197 | * **Path**: /social/recommend 198 | * **Params**: 无需参数 199 | 200 | * **Return**: 201 | 202 | field | required | type | description 203 | ------|----------|-----------|----------------------- 204 | users | Yes | User List | 用户数据列表 205 | 206 | 示例: 207 | 208 | ```json 209 | { 210 | "code": 0, 211 | "data": { 212 | "users": [ 213 | {"uid": 123, "nickname": "Da", "age": 21, ...}, 214 | {"uid": 456, "nickname": "Miao", "age": 21, ...}, 215 | ... 216 | ], 217 | } 218 | } 219 | ``` 220 | 221 | 222 | 2. 喜欢 223 | * **Description**: 224 | * **Method**: POST 225 | * **Path**: /social/like 226 | * **Params**: 227 | 228 | field | required | type | description 229 | ------------|----------|------|----------------------- 230 | stranger_id | Yes | int | 被滑用户的 uid 231 | 232 | * **Return**: 233 | 234 | field | required | type | description 235 | --------|----------|------|----------------------- 236 | matched | Yes | bool | 是否与此用户匹配 237 | 238 | 3. 超级喜欢 239 | * **Description**: 240 | * **Method**: POST 241 | * **Path**: /social/superlike 242 | * **Params**: 243 | 244 | field | required | type | description 245 | ------------|----------|------|----------------------- 246 | stranger_id | Yes | int | 被滑用户的 uid 247 | 248 | * **Return**: 249 | 250 | field | required | type | description 251 | --------|----------|------|----------------------- 252 | matched | Yes | bool | 是否与此用户匹配 253 | 254 | 4. 不喜欢 255 | * **Description**: 256 | * **Method**: POST 257 | * **Path**: /social/dislike 258 | * **Params**: 259 | 260 | field | required | type | description 261 | ------------|----------|------|----------------------- 262 | stranger_id | Yes | int | 被滑用户的 uid 263 | 264 | * **Return**: 265 | 266 | data 为 null 267 | 268 | 5. 反悔 269 | * **Description**: 270 | * **Method**: POST 271 | * **Path**: /social/rewind 272 | * **Params**: 273 | 274 | field | required | type | description 275 | ------------|----------|------|----------------------- 276 | stranger_id | Yes | int | 被滑用户的 uid 277 | 278 | * **Return**: 279 | 280 | data 为 null 281 | 282 | 6. 查看谁喜欢过我 283 | * **Description**: 284 | * **Method**: GET 285 | * **Path**: social/likedme 286 | * **Params**: 无需参数 287 | 288 | * **Return**: 289 | 290 | field | required | type | description 291 | ------|----------|-----------|----------------------- 292 | users | Yes | User List | 喜欢过我的用户数据列表 293 | 294 | 示例: 295 | ```json 296 | { 297 | "code": 0, 298 | "data": { 299 | "users": [ 300 | {"uid": 123, "nickname": "Da", "age": 21, ...}, 301 | {"uid": 456, "nickname": "Miao", "age": 21, ...}, 302 | ... 303 | ] 304 | } 305 | } 306 | ``` 307 | 308 | 7. 查看好友列表 309 | * **Description**: 310 | * **Method**: GET 311 | * **Path**: social/friends 312 | * **Params**: 无需参数 313 | 314 | * **Return**: 315 | 316 | field | required | type | description 317 | --------|----------|-----------|----------------------- 318 | friends | Yes | User List | 我的好友数据列表 319 | 320 | 示例: 321 | ```json 322 | { 323 | "code": 0, 324 | "data": { 325 | "friends": [ 326 | {"uid": 123, "nickname": "Da", "age": 21, ...}, 327 | {"uid": 456, "nickname": "Miao", "age": 21, ...}, 328 | ... 329 | ] 330 | } 331 | } 332 | ``` 333 | 334 | 8. 断绝好友关系 335 | * **Description**: 336 | * **Method**: POST 337 | * **Path**: social/break_off 338 | * **Params**: 339 | 340 | field | required | type | description 341 | ------------|----------|------|----------------------- 342 | stranger_id | Yes | int | 要绝交的用户 uid 343 | 344 | * **Return**: 345 | 346 | data 为 null 347 | -------------------------------------------------------------------------------- /tutorial/swiper/frontend/img/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/frontend/img/help.png -------------------------------------------------------------------------------- /tutorial/swiper/frontend/img/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/frontend/img/history.png -------------------------------------------------------------------------------- /tutorial/swiper/frontend/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/frontend/img/icon.png -------------------------------------------------------------------------------- /tutorial/swiper/frontend/img/like-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/frontend/img/like-txt.png -------------------------------------------------------------------------------- /tutorial/swiper/frontend/img/like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/frontend/img/like.png -------------------------------------------------------------------------------- /tutorial/swiper/frontend/img/nope-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/frontend/img/nope-txt.png -------------------------------------------------------------------------------- /tutorial/swiper/frontend/img/nope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/frontend/img/nope.png -------------------------------------------------------------------------------- /tutorial/swiper/frontend/img/super-like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/frontend/img/super-like.png -------------------------------------------------------------------------------- /tutorial/swiper/frontend/img/super-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/tutorial/swiper/frontend/img/super-txt.png -------------------------------------------------------------------------------- /tutorial/swiper/frontend/info.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Login 9 | 10 | 11 | 12 | 39 | 40 | 41 | 42 |
43 |

Swiper

44 | 45 |
46 | 47 | 48 |
49 | 50 |
51 | 52 | 55 | 56 | 57 | 使用微博账号登录 58 | 59 | 60 |
61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /tutorial/swiper/frontend/js/init.js: -------------------------------------------------------------------------------- 1 | axios.defaults.withCredentials = true; 2 | 3 | const request = axios.create({ 4 | baseURL: 'http://127.0.0.1:9000/', 5 | headers:{ 6 | "Access-Control-Allow-Headers":"Origin, X-Requested-With, Content-Type, Accept", 7 | 'Content-Type':'application/x-www-form-urlencoded', 8 | "Access-Control-Allow-Credentials": true, 9 | "Access-Control-Allow-Origin": "http://127.0.0.1:8000", 10 | "Access-Control-Allow-Methods": "POST,GET,PUT,DELETE,OPTIONS", 11 | }, 12 | transformRequest: [function (data) { 13 | data = Qs.stringify(data); 14 | return data; 15 | }] 16 | }); 17 | -------------------------------------------------------------------------------- /tutorial/swiper/frontend/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Login 9 | 10 | 11 | 12 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 |

Swiper

48 | 49 |
50 | 51 |
52 |
53 | 56 |
57 | 58 |
59 | 63 |
64 | 65 | 68 | 69 | 70 | 使用微博账号登录 71 | 72 |
73 |
74 | 75 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /tutorial/swiper/frontend/swiper.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vue-Swiper Simple 7 | 74 | 75 | 76 |
77 | 86 | 91 | 92 | 93 | 94 | 95 |
96 | 97 | 98 | 99 | 100 | 101 |
102 |
103 | 104 | 105 | 106 | 107 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /tutorial/swiper/requirements.txt: -------------------------------------------------------------------------------- 1 | celery==4.2.1 2 | Django==1.11.15 3 | django-cors-headers==2.4.0 4 | django-redis==4.9.0 5 | gunicorn==19.9.0 6 | ipython==6.5.0 7 | Pillow==5.2.0 8 | qiniu==7.2.2 9 | redis==2.10.6 10 | requests==2.20.0 11 | tornado==5.1.1 12 | tornado-redis==2.4.18 13 | -------------------------------------------------------------------------------- /zip/test.txt: -------------------------------------------------------------------------------- 1 | 123 -------------------------------------------------------------------------------- /zip/test.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/python-tutorial/02ad3b7cd4ac179759e1b956cc707989575ca43b/zip/test.zip -------------------------------------------------------------------------------- /zip/zip.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | zFile = zipfile.ZipFile("./test.zip"); 3 | zFile.extractall("./",pwd="123"); --------------------------------------------------------------------------------