├── .gitignore ├── README.md ├── api ├── __init__.py └── user.py ├── app.py ├── common ├── __init__.py ├── md5_operate.py ├── mysql_operate.py └── redis_operate.py ├── config ├── __init__.py └── setting.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flaskDemo 2 | 3 | 本接口项目的技术选型:Python+Flask+MySQL+Redis,通过 Python+Falsk 来开发接口,使用 MySQL 来存储用户信息,使用 Redis 用于存储token,目前为纯后端接口,暂无前端界面,可通过 Postman、Jmeter、Fiddler 等工具访问请求接口。 4 | 5 | ## 项目部署 6 | 7 | 首先,下载项目源码后,在根目录下找到 requirements.txt 文件,然后通过 pip 工具安装 requirements.txt 依赖,执行命令: 8 | 9 | ``` 10 | pip3 install -r requirements.txt 11 | ``` 12 | 13 | 接着,将项目部署起来,在本项目中其实就是利用 Python 执行 app.py 文件,以下为我在Linux上的部署命令。 14 | 15 | ``` 16 | # /root/flaskDemo/app.py表示项目根路径下的app.py启动入口文件路径 17 | # /root/flaskDemo/flaskDemo.log表示输出的日志文件路径 18 | nohup python3 /root/flaskDemo/app.py >/root/flaskDemo/flaskDemo.log 2>&1 & 19 | ``` 20 | 21 | ## 数据库设计 22 | 23 | 数据库建表语句如下: 24 | 25 | ``` 26 | CREATE TABLE `user` ( 27 | `id` int(11) NOT NULL AUTO_INCREMENT, 28 | `username` varchar(20) NOT NULL, 29 | `password` varchar(255) NOT NULL, 30 | `role` tinyint(1) NOT NULL, 31 | `sex` tinyint(1) DEFAULT NULL, 32 | `telephone` varchar(255) NOT NULL, 33 | `address` varchar(255) DEFAULT NULL, 34 | PRIMARY KEY (`id`), 35 | UNIQUE KEY `telephone` (`telephone`) 36 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 37 | ``` 38 | 39 | user表中各字段对应含义如下: 40 | 41 | ``` 42 | id:用户id号,自增长 43 | username:用户名 44 | password:密码 45 | role:用户角色,0表示管理员用户,1表示普通用户 46 | sex:性别,0表示男性,1表示女性,允许为空 47 | telephone:手机号 48 | address:联系地址,允许为空 49 | ``` 50 | 51 | ## 接口请求示例 52 | 53 | - 获取所有用户接口请求示例(可直接在浏览器输入栏请求): 54 | 55 | ``` 56 | 请求方式:GET 57 | 请求地址:http://127.0.0.1:9999/users 58 | ``` 59 | 60 | - 获取wintest用户接口请求示例(可直接在浏览器输入栏请求): 61 | 62 | ``` 63 | 请求方式:GET 64 | 请求地址:http://127.0.0.1:9999/users/wintest 65 | ``` 66 | 67 | - 用户注册接口请求示例: 68 | 69 | ``` 70 | 请求方式:POST 71 | 请求地址:http://127.0.0.1:9999/register 72 | 请求头: 73 | Content-Type: application/json 74 | 75 | Body:{"username": "wintest5", "password": "123456", "sex": "1", "telephone":"13500010005", "address": "上海市黄浦区"} 76 | ``` 77 | 78 | - 用户登录接口请求示例: 79 | 80 | ``` 81 | 请求方式:POST 82 | 请求地址:http://127.0.0.1:9999/login 83 | 请求头: 84 | Content-Type: application/x-www-form-urlencoded 85 | 86 | Body:username=wintest&password=123456 87 | ``` 88 | 89 | - 修改用户接口请求示例( token 可以从用户登录成功后的接口返回数据中获取): 90 | 91 | ``` 92 | 请求方式:PUT 93 | 请求地址:http://127.0.0.1:9999/update/user/3 94 | 请求头: 95 | Content-Type: application/json 96 | 97 | Body:{"admin_user": "wintest", "token": "f54f9d6ebba2c75d45ba00a8832cb593", "sex": "1", "address": "广州市天河区", "password": "12345678", "telephone": "13500010003"} 98 | ``` 99 | 100 | - 删除用户接口请求示例( token 可以从用户登录成功后的接口返回数据中获取):: 101 | 102 | ``` 103 | 请求方式:POST 104 | 请求地址:http://127.0.0.1:9999/delete/user/test 105 | 请求头: 106 | Content-Type: application/json 107 | 108 | Body:{"admin_user": "wintest", "token": "wintest1587830406"} 109 | ``` 110 | -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wintests/flaskDemo/7cbf8e1da1fe5c0b39801f8189096b51d56eed76/api/__init__.py -------------------------------------------------------------------------------- /api/user.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, request 2 | from common.mysql_operate import db 3 | from common.redis_operate import redis_db 4 | from common.md5_operate import get_md5 5 | import re, time 6 | 7 | app = Flask(__name__) 8 | app.config["JSON_AS_ASCII"] = False # jsonify返回的中文正常显示 9 | 10 | 11 | @app.route('/') 12 | def hello_world(): 13 | return 'Hello World!' 14 | 15 | 16 | @app.route("/users", methods=["GET"]) 17 | def get_all_users(): 18 | """获取所有用户信息""" 19 | sql = "SELECT * FROM user" 20 | data = db.select_db(sql) 21 | print("获取所有用户信息 == >> {}".format(data)) 22 | return jsonify({"code": 0, "data": data, "msg": "查询成功"}) 23 | 24 | 25 | @app.route("/users/", methods=["GET"]) 26 | def get_user(username): 27 | """获取某个用户信息""" 28 | sql = "SELECT * FROM user WHERE username = '{}'".format(username) 29 | data = db.select_db(sql) 30 | print("获取 {} 用户信息 == >> {}".format(username, data)) 31 | if data: 32 | return jsonify({"code": 0, "data": data, "msg": "查询成功"}) 33 | return jsonify({"code": "1004", "msg": "查不到相关用户的信息"}) 34 | 35 | 36 | @app.route("/register", methods=['POST']) 37 | def user_register(): 38 | """注册用户""" 39 | username = request.json.get("username", "").strip() # 用户名 40 | password = request.json.get("password", "").strip() # 密码 41 | sex = request.json.get("sex", "0").strip() # 性别,默认为0(男性) 42 | telephone = request.json.get("telephone", "").strip() # 手机号 43 | address = request.json.get("address", "").strip() # 地址,默认为空串 44 | if username and password and telephone: # 注意if条件中 "" 也是空, 按False处理 45 | sql1 = "SELECT username FROM user WHERE username = '{}'".format(username) 46 | res1 = db.select_db(sql1) 47 | print("查询到用户名 ==>> {}".format(res1)) 48 | sql2 = "SELECT telephone FROM user WHERE telephone = '{}'".format(telephone) 49 | res2 = db.select_db(sql2) 50 | print("查询到手机号 ==>> {}".format(res2)) 51 | if res1: 52 | return jsonify({"code": 2002, "msg": "用户名已存在,注册失败!!!"}) 53 | elif not (sex == "0" or sex == "1"): 54 | return jsonify({"code": 2003, "msg": "输入的性别只能是 0(男) 或 1(女)!!!"}) 55 | elif not (len(telephone) == 11 and re.match("^1[3,5,7,8]\d{9}$", telephone)): 56 | return jsonify({"code": 2004, "msg": "手机号格式不正确!!!"}) 57 | elif res2: 58 | return jsonify({"code": 2005, "msg": "手机号已被注册!!!"}) 59 | else: 60 | password = get_md5(username, password) # 把传入的明文密码通过MD5加密变为密文,然后再进行注册 61 | sql3 = "INSERT INTO user(username, password, role, sex, telephone, address) " \ 62 | "VALUES('{}', '{}', '1', '{}', '{}', '{}')".format(username, password, sex, telephone, address) 63 | db.execute_db(sql3) 64 | print("新增用户信息SQL ==>> {}".format(sql3)) 65 | return jsonify({"code": 0, "msg": "恭喜,注册成功!"}) 66 | else: 67 | return jsonify({"code": 2001, "msg": "用户名/密码/手机号不能为空,请检查!!!"}) 68 | 69 | 70 | @app.route("/login", methods=['POST']) 71 | def user_login(): 72 | """登录用户""" 73 | username = request.values.get("username", "").strip() 74 | password = request.values.get("password", "").strip() 75 | if username and password: # 注意if条件中空串 "" 也是空, 按False处理 76 | sql1 = "SELECT username FROM user WHERE username = '{}'".format(username) 77 | res1 = db.select_db(sql1) 78 | print("查询到用户名 ==>> {}".format(res1)) 79 | if not res1: 80 | return jsonify({"code": 1003, "msg": "用户名不存在!!!"}) 81 | md5_password = get_md5(username, password) # 把传入的明文密码通过MD5加密变为密文 82 | sql2 = "SELECT * FROM user WHERE username = '{}' and password = '{}'".format(username, md5_password) 83 | res2 = db.select_db(sql2) 84 | print("获取 {} 用户信息 == >> {}".format(username, res2)) 85 | if res2: 86 | timeStamp = int(time.time()) # 获取当前时间戳 87 | # token = "{}{}".format(username, timeStamp) 88 | token = get_md5(username, str(timeStamp)) # MD5加密后得到token 89 | redis_db.handle_redis_token(username, token) # 把token放到redis中存储 90 | login_info = { # 构造一个字段,将 id/username/token/login_time 返回 91 | "id": res2[0]["id"], 92 | "username": username, 93 | "token": token, 94 | "login_time": time.strftime("%Y/%m/%d %H:%M:%S") 95 | } 96 | return jsonify({"code": 0, "login_info": login_info, "msg": "恭喜,登录成功!"}) 97 | return jsonify({"code": 1002, "msg": "用户名或密码错误!!!"}) 98 | else: 99 | return jsonify({"code": 1001, "msg": "用户名或密码不能为空!!!"}) 100 | 101 | @app.route("/update/user/", methods=['PUT']) 102 | def user_update(id): # id为准备修改的用户ID 103 | """修改用户信息""" 104 | admin_user = request.json.get("admin_user", "").strip() # 当前登录的管理员用户 105 | token = request.json.get("token", "").strip() # token口令 106 | new_password = request.json.get("password", "").strip() # 新的密码 107 | new_sex = request.json.get("sex", "0").strip() # 新的性别,如果参数不传sex,那么默认为0(男性) 108 | new_telephone = request.json.get("telephone", "").strip() # 新的手机号 109 | new_address = request.json.get("address", "").strip() # 新的联系地址,默认为空串 110 | if admin_user and token and new_password and new_telephone: # 注意if条件中空串 "" 也是空, 按False处理 111 | if not (new_sex == "0" or new_sex == "1"): 112 | return jsonify({"code": 4007, "msg": "输入的性别只能是 0(男) 或 1(女)!!!"}) 113 | elif not (len(new_telephone) == 11 and re.match("^1[3,5,7,8]\d{9}$", new_telephone)): 114 | return jsonify({"code": 4008, "msg": "手机号格式不正确!!!"}) 115 | else: 116 | redis_token = redis_db.handle_redis_token(admin_user) # 从redis中取token 117 | if redis_token: 118 | if redis_token == token: # 如果从redis中取到的token不为空,且等于请求body中的token 119 | sql1 = "SELECT role FROM user WHERE username = '{}'".format(admin_user) 120 | res1 = db.select_db(sql1) 121 | print("根据用户名 【 {} 】 查询到用户类型 == >> {}".format(admin_user, res1)) 122 | user_role = res1[0]["role"] 123 | if user_role == 0: # 如果当前登录用户是管理员用户 124 | sql2 = "SELECT * FROM user WHERE id = '{}'".format(id) 125 | res2 = db.select_db(sql2) 126 | print("根据用户ID 【 {} 】 查询到用户信息 ==>> {}".format(id, res2)) 127 | sql3 = "SELECT telephone FROM user WHERE telephone = '{}'".format(new_telephone) 128 | res3 = db.select_db(sql3) 129 | print("返回结果:{}".format(res3)) 130 | print("查询到手机号 ==>> {}".format(res3)) 131 | if not res2: # 如果要修改的用户不存在于数据库中,res2为空 132 | return jsonify({"code": 4005, "msg": "修改的用户ID不存在,无法进行修改,请检查!!!"}) 133 | elif res3: # 如果要修改的手机号已经存在于数据库中,res3非空 134 | return jsonify({"code": 4006, "msg": "手机号已被注册,无法进行修改,请检查!!!"}) 135 | else: 136 | # 如果请求参数不传address,那么address字段不会被修改,仍为原值 137 | if not new_address: 138 | new_address = res2[0]["address"] 139 | # 把传入的明文密码通过MD5加密变为密文 140 | new_password = get_md5(res2[0]["username"], new_password) 141 | sql3 = "UPDATE user SET password = '{}', sex = '{}', telephone = '{}', address = '{}' " \ 142 | "WHERE id = {}".format(new_password, new_sex, new_telephone, new_address, id) 143 | db.execute_db(sql3) 144 | print("修改用户信息SQL ==>> {}".format(sql3)) 145 | return jsonify({"code": 0, "msg": "恭喜,修改用户信息成功!"}) 146 | else: 147 | return jsonify({"code": 4004, "msg": "当前用户不是管理员用户,无法进行操作,请检查!!!"}) 148 | else: 149 | return jsonify({"code": 4003, "msg": "token口令不正确,请检查!!!"}) 150 | else: 151 | return jsonify({"code": 4002, "msg": "当前用户未登录,请检查!!!"}) 152 | else: 153 | return jsonify({"code": 4001, "msg": "管理员用户/token口令/密码/手机号不能为空,请检查!!!"}) 154 | 155 | @app.route("/delete/user/", methods=['POST']) 156 | def user_delete(username): 157 | admin_user = request.json.get("admin_user", "").strip() # 当前登录的管理员用户 158 | token = request.json.get("token", "").strip() # token口令 159 | if admin_user and token: 160 | redis_token = redis_db.handle_redis_token(admin_user) # 从redis中取token 161 | if redis_token: 162 | if redis_token == token: # 如果从redis中取到的token不为空,且等于请求body中的token 163 | sql1 = "SELECT role FROM user WHERE username = '{}'".format(admin_user) 164 | res1 = db.select_db(sql1) 165 | print("根据用户名 【 {} 】 查询到用户类型 == >> {}".format(admin_user, res1)) 166 | user_role = res1[0]["role"] 167 | if user_role == 0: # 如果当前登录用户是管理员用户 168 | sql2 = "SELECT * FROM user WHERE username = '{}'".format(username) 169 | res2 = db.select_db(sql2) 170 | print(sql2) 171 | print("根据用户名 【 {} 】 查询到用户信息 ==>> {}".format(username, res2)) 172 | if not res2: # 如果要删除的用户不存在于数据库中,res2为空 173 | return jsonify({"code": 3005, "msg": "删除的用户名不存在,无法进行删除,请检查!!!"}) 174 | elif res2[0]["role"] == 0: # 如果要删除的用户是管理员用户,则不允许删除 175 | return jsonify({"code": 3006, "msg": "用户名:【 {} 】,该用户不允许删除!!!".format(username)}) 176 | else: 177 | sql3 = "DELETE FROM user WHERE username = '{}'".format(username) 178 | db.execute_db(sql3) 179 | print("删除用户信息SQL ==>> {}".format(sql3)) 180 | return jsonify({"code": 0, "msg": "恭喜,删除用户信息成功!"}) 181 | else: 182 | return jsonify({"code": 3004, "msg": "当前用户不是管理员用户,无法进行操作,请检查!!!"}) 183 | else: 184 | return jsonify({"code": 3003, "msg": "token口令不正确,请检查!!!"}) 185 | else: 186 | return jsonify({"code": 3002, "msg": "当前用户未登录,请检查!!!"}) 187 | else: 188 | return jsonify({"code": 3001, "msg": "管理员用户/token口令不能为空,请检查!!!"}) -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | from config.setting import SERVER_PORT 3 | from api.user import app 4 | 5 | # 项目根路径 6 | BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 7 | sys.path.insert(0, BASE_PATH) # 将项目根路径临时加入环境变量,程序退出后失效 8 | 9 | if __name__ == '__main__': 10 | # host为主机ip地址,port指定访问端口号,debug=True设置调试模式打开 11 | app.run(host="0.0.0.0", port=SERVER_PORT, debug=True) 12 | -------------------------------------------------------------------------------- /common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wintests/flaskDemo/7cbf8e1da1fe5c0b39801f8189096b51d56eed76/common/__init__.py -------------------------------------------------------------------------------- /common/md5_operate.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from config.setting import MD5_SALT 3 | 4 | 5 | def get_md5(username, str): 6 | """MD5加密处理""" 7 | str = username + str + MD5_SALT # 把用户名也作为str加密的一部分 8 | md5 = hashlib.md5() # 创建md5对象 9 | md5.update(str.encode("utf-8")) # Python3中需要先转换为 bytes 类型,才能加密 10 | return md5.hexdigest() # 返回密文 -------------------------------------------------------------------------------- /common/mysql_operate.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | from config.setting import MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWD, MYSQL_DB 3 | 4 | class MysqlDb(): 5 | 6 | def __init__(self, host, port, user, passwd, db): 7 | # 建立数据库连接 8 | self.conn = pymysql.connect( 9 | host=host, 10 | port=port, 11 | user=user, 12 | passwd=passwd, 13 | db=db, 14 | autocommit=True 15 | ) 16 | # 通过 cursor() 创建游标对象,并让查询结果以字典格式输出 17 | self.cur = self.conn.cursor(cursor=pymysql.cursors.DictCursor) 18 | 19 | def __del__(self): # 对象资源被释放时触发,在对象即将被删除时的最后操作 20 | # 关闭游标 21 | self.cur.close() 22 | # 关闭数据库连接 23 | self.conn.close() 24 | 25 | def select_db(self, sql): 26 | """查询""" 27 | # 检查连接是否断开,如果断开就进行重连 28 | self.conn.ping(reconnect=True) 29 | # 使用 execute() 执行sql 30 | self.cur.execute(sql) 31 | # 使用 fetchall() 获取查询结果 32 | data = self.cur.fetchall() 33 | return data 34 | 35 | def execute_db(self, sql): 36 | """更新/新增/删除""" 37 | try: 38 | # 检查连接是否断开,如果断开就进行重连 39 | self.conn.ping(reconnect=True) 40 | # 使用 execute() 执行sql 41 | self.cur.execute(sql) 42 | # 提交事务 43 | self.conn.commit() 44 | except Exception as e: 45 | print("操作出现错误:{}".format(e)) 46 | # 回滚所有更改 47 | self.conn.rollback() 48 | 49 | db = MysqlDb(MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWD, MYSQL_DB) -------------------------------------------------------------------------------- /common/redis_operate.py: -------------------------------------------------------------------------------- 1 | import redis 2 | from config.setting import REDIS_HOST, REDIS_PORT, REDIS_PASSWD, EXPIRE_TIME 3 | 4 | 5 | class RedisDb(): 6 | 7 | def __init__(self, host, port, passwd): 8 | # 建立数据库连接 9 | self.r = redis.Redis( 10 | host=host, 11 | port=port, 12 | password=passwd, 13 | decode_responses=True # get() 得到字符串类型的数据 14 | ) 15 | 16 | def handle_redis_token(self, key, value=None): 17 | if value: # 如果value非空,那么就设置key和value,EXPIRE_TIME为过期时间 18 | self.r.set(key, value, ex=EXPIRE_TIME) 19 | else: # 如果value为空,那么直接通过key从redis中取值 20 | redis_token = self.r.get(key) 21 | return redis_token 22 | 23 | 24 | redis_db = RedisDb(REDIS_HOST, REDIS_PORT, REDIS_PASSWD) -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wintests/flaskDemo/7cbf8e1da1fe5c0b39801f8189096b51d56eed76/config/__init__.py -------------------------------------------------------------------------------- /config/setting.py: -------------------------------------------------------------------------------- 1 | # 服务端口配置 2 | SERVER_PORT = 9999 3 | 4 | # MySQL配置 5 | MYSQL_HOST = "192.168.89.128" 6 | MYSQL_PORT = 3306 7 | MYSQL_USER = "root" 8 | MYSQL_PASSWD = "123456" 9 | MYSQL_DB = "flask_demo" 10 | 11 | # Redis配置 12 | REDIS_HOST = "192.168.89.128" 13 | REDIS_PORT = 6379 14 | REDIS_PASSWD = "123456" 15 | # token过期时间(单位:秒) 16 | EXPIRE_TIME = 600 17 | 18 | # MD5加密盐值 19 | MD5_SALT = "test2020#%*" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyMySQL==0.9.3 2 | Flask==1.0.3 3 | redis==3.4.1 4 | --------------------------------------------------------------------------------