├── .gitignore ├── .vscode └── launch.json ├── Dockerfile ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── app ├── .env ├── __init__.py ├── config.py ├── log.py ├── main.py ├── models │ ├── __init__.py │ └── ip_count.py ├── routes │ ├── __init__.py │ ├── girls.py │ ├── home.py │ └── log_list.py ├── static │ ├── css │ │ └── pc.css │ └── image │ │ ├── bg.gif │ │ └── favicon.ico └── templates │ ├── index.html │ └── log_list.html ├── docker-compose.yaml ├── docs └── image │ ├── flask-deploy.png │ ├── run-debug1.png │ ├── run-debug2.png │ ├── run-python.png │ ├── screenshot.png │ └── select-pipenv.png ├── gunicorn.conf.py ├── nginx_flask.conf ├── requirements.txt ├── script ├── compose-deploy.sh ├── docker-deploy.sh └── run.sh └── supervisord.conf /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | logs 3 | *.db 4 | migrations 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Flask", 9 | "type": "python", 10 | "request": "launch", 11 | "module": "flask", 12 | "env": { 13 | "FLASK_APP": "app:create_app('development')", 14 | "FLASK_ENV": "development" 15 | }, 16 | "args": [ 17 | "run" 18 | ], 19 | "jinja": true 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM python:3.7-slim-buster 3 | 4 | RUN mkdir /deploy 5 | # 设置工作目录(后续都不需要指定完整路径了,默认工作目录/deploy) 6 | WORKDIR /deploy 7 | 8 | # 复制源码 (使用映射代替copy) 9 | #COPY ./app ./deploy/app 10 | 11 | # 复制发布相关脚本 12 | COPY requirements.txt requirements.txt 13 | COPY gunicorn.conf.py gunicorn.conf.py 14 | 15 | # 安装依赖 16 | RUN pip3 install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple 17 | RUN pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple 18 | RUN pip3 install gunicorn -i https://pypi.tuna.tsinghua.edu.cn/simple 19 | RUN pip3 install gevent -i https://pypi.tuna.tsinghua.edu.cn/simple 20 | 21 | # 更新apt源 22 | RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list 23 | RUN apt-get update && apt-get install -y nginx supervisor 24 | 25 | # 配置nginx 26 | RUN rm /etc/nginx/sites-enabled/default 27 | COPY nginx_flask.conf /etc/nginx/sites-available/ 28 | RUN ln -s /etc/nginx/sites-available/nginx_flask.conf /etc/nginx/sites-enabled/nginx_flask.conf 29 | # 是否后台启动:决定启动nginx命令是否block,因为supervisor不能监控后台进程,command 不能为后台运行命令 30 | # 用supervisor启动nginx的话需要关掉daemon 31 | RUN echo "daemon off;" >> /etc/nginx/nginx.conf 32 | 33 | # supervisord https://www.jianshu.com/p/0b9054b33db3 34 | RUN mkdir -p /var/log/supervisor 35 | COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf 36 | 37 | # 复制运行脚本 38 | COPY ./script/run.sh ./run.sh 39 | RUN chmod +x ./run.sh 40 | 41 | # 设置环境变量(给flask迁移db用) 42 | ENV FLASK_APP="app:create_app('production')" 43 | ENV FLASK_ENV="production" 44 | 45 | # 运行(最后这条CMD命令需要阻塞,否则docker启动后接着退出) 46 | CMD ["./run.sh"] 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Barry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.tuna.tsinghua.edu.cn/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | flask = "1.1.2" 8 | flask-apscheduler = "1.11.0" 9 | flask-sqlalchemy = "2.4.4" 10 | flask-migrate = "2.7.0" 11 | pymysql = "1.0.2" 12 | python-dotenv = "0.19.0" 13 | requests = "2.25.1" 14 | 15 | [dev-packages] 16 | 17 | [requires] 18 | python_version = "3.7" -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "6ef3bc7e7963b97fd86df9291d210b63b5f0e9297dea6203429493496cc79333" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.tuna.tsinghua.edu.cn/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "alembic": { 20 | "hashes": [ 21 | "sha256:a21fedebb3fb8f6bbbba51a11114f08c78709377051384c9c5ead5705ee93a51", 22 | "sha256:e78be5b919f5bb184e3e0e2dd1ca986f2362e29a2bc933c446fe89f39dbe4e9c" 23 | ], 24 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 25 | "version": "==1.6.5" 26 | }, 27 | "apscheduler": { 28 | "hashes": [ 29 | "sha256:1cab7f2521e107d07127b042155b632b7a1cd5e02c34be5a28ff62f77c900c6a", 30 | "sha256:c06cc796d5bb9eb3c4f77727f6223476eb67749e7eea074d1587550702a7fbe3" 31 | ], 32 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 33 | "version": "==3.7.0" 34 | }, 35 | "certifi": { 36 | "hashes": [ 37 | "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", 38 | "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" 39 | ], 40 | "version": "==2021.5.30" 41 | }, 42 | "charset-normalizer": { 43 | "hashes": [ 44 | "sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1", 45 | "sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12" 46 | ], 47 | "markers": "python_version >= '3'", 48 | "version": "==2.0.3" 49 | }, 50 | "click": { 51 | "hashes": [ 52 | "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", 53 | "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" 54 | ], 55 | "markers": "python_version >= '3.6'", 56 | "version": "==8.0.1" 57 | }, 58 | "colorama": { 59 | "hashes": [ 60 | "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", 61 | "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" 62 | ], 63 | "markers": "platform_system == 'Windows'", 64 | "version": "==0.4.4" 65 | }, 66 | "flask": { 67 | "hashes": [ 68 | "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55", 69 | "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9" 70 | ], 71 | "index": "pypi", 72 | "version": "==2.0.1" 73 | }, 74 | "flask-apscheduler": { 75 | "hashes": [ 76 | "sha256:b9fe174b90d201d8beeba5522b023208f7bb6e2583fc02fea4be4bce5ee8f9e5" 77 | ], 78 | "index": "pypi", 79 | "version": "==1.12.2" 80 | }, 81 | "flask-migrate": { 82 | "hashes": [ 83 | "sha256:4d42e8f861d78cb6e9319afcba5bf76062e5efd7784184dd2a1cccd9de34a702", 84 | "sha256:df9043d2050df3c0e0f6313f6b529b62c837b6033c20335e9d0b4acdf2c40e23" 85 | ], 86 | "index": "pypi", 87 | "version": "==3.0.1" 88 | }, 89 | "flask-sqlalchemy": { 90 | "hashes": [ 91 | "sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912", 92 | "sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390" 93 | ], 94 | "index": "pypi", 95 | "version": "==2.5.1" 96 | }, 97 | "greenlet": { 98 | "hashes": [ 99 | "sha256:03f28a5ea20201e70ab70518d151116ce939b412961c33827519ce620957d44c", 100 | "sha256:06d7ac89e6094a0a8f8dc46aa61898e9e1aec79b0f8b47b2400dd51a44dbc832", 101 | "sha256:06ecb43b04480e6bafc45cb1b4b67c785e183ce12c079473359e04a709333b08", 102 | "sha256:096cb0217d1505826ba3d723e8981096f2622cde1eb91af9ed89a17c10aa1f3e", 103 | "sha256:0c557c809eeee215b87e8a7cbfb2d783fb5598a78342c29ade561440abae7d22", 104 | "sha256:0de64d419b1cb1bfd4ea544bedea4b535ef3ae1e150b0f2609da14bbf48a4a5f", 105 | "sha256:14927b15c953f8f2d2a8dffa224aa78d7759ef95284d4c39e1745cf36e8cdd2c", 106 | "sha256:16183fa53bc1a037c38d75fdc59d6208181fa28024a12a7f64bb0884434c91ea", 107 | "sha256:206295d270f702bc27dbdbd7651e8ebe42d319139e0d90217b2074309a200da8", 108 | "sha256:22002259e5b7828b05600a762579fa2f8b33373ad95a0ee57b4d6109d0e589ad", 109 | "sha256:2325123ff3a8ecc10ca76f062445efef13b6cf5a23389e2df3c02a4a527b89bc", 110 | "sha256:258f9612aba0d06785143ee1cbf2d7361801c95489c0bd10c69d163ec5254a16", 111 | "sha256:3096286a6072553b5dbd5efbefc22297e9d06a05ac14ba017233fedaed7584a8", 112 | "sha256:3d13da093d44dee7535b91049e44dd2b5540c2a0e15df168404d3dd2626e0ec5", 113 | "sha256:408071b64e52192869129a205e5b463abda36eff0cebb19d6e63369440e4dc99", 114 | "sha256:598bcfd841e0b1d88e32e6a5ea48348a2c726461b05ff057c1b8692be9443c6e", 115 | "sha256:5d928e2e3c3906e0a29b43dc26d9b3d6e36921eee276786c4e7ad9ff5665c78a", 116 | "sha256:5f75e7f237428755d00e7460239a2482fa7e3970db56c8935bd60da3f0733e56", 117 | "sha256:60848099b76467ef09b62b0f4512e7e6f0a2c977357a036de602b653667f5f4c", 118 | "sha256:6b1d08f2e7f2048d77343279c4d4faa7aef168b3e36039cba1917fffb781a8ed", 119 | "sha256:70bd1bb271e9429e2793902dfd194b653221904a07cbf207c3139e2672d17959", 120 | "sha256:76ed710b4e953fc31c663b079d317c18f40235ba2e3d55f70ff80794f7b57922", 121 | "sha256:7920e3eccd26b7f4c661b746002f5ec5f0928076bd738d38d894bb359ce51927", 122 | "sha256:7db68f15486d412b8e2cfcd584bf3b3a000911d25779d081cbbae76d71bd1a7e", 123 | "sha256:8833e27949ea32d27f7e96930fa29404dd4f2feb13cce483daf52e8842ec246a", 124 | "sha256:944fbdd540712d5377a8795c840a97ff71e7f3221d3fddc98769a15a87b36131", 125 | "sha256:9a6b035aa2c5fcf3dbbf0e3a8a5bc75286fc2d4e6f9cfa738788b433ec894919", 126 | "sha256:9bdcff4b9051fb1aa4bba4fceff6a5f770c6be436408efd99b76fc827f2a9319", 127 | "sha256:a9017ff5fc2522e45562882ff481128631bf35da444775bc2776ac5c61d8bcae", 128 | "sha256:aa4230234d02e6f32f189fd40b59d5a968fe77e80f59c9c933384fe8ba535535", 129 | "sha256:ad80bb338cf9f8129c049837a42a43451fc7c8b57ad56f8e6d32e7697b115505", 130 | "sha256:adb94a28225005890d4cf73648b5131e885c7b4b17bc762779f061844aabcc11", 131 | "sha256:b3090631fecdf7e983d183d0fad7ea72cfb12fa9212461a9b708ff7907ffff47", 132 | "sha256:b33b51ab057f8a20b497ffafdb1e79256db0c03ef4f5e3d52e7497200e11f821", 133 | "sha256:b97c9a144bbeec7039cca44df117efcbeed7209543f5695201cacf05ba3b5857", 134 | "sha256:be13a18cec649ebaab835dff269e914679ef329204704869f2f167b2c163a9da", 135 | "sha256:be9768e56f92d1d7cd94185bab5856f3c5589a50d221c166cc2ad5eb134bd1dc", 136 | "sha256:c1580087ab493c6b43e66f2bdd165d9e3c1e86ef83f6c2c44a29f2869d2c5bd5", 137 | "sha256:c35872b2916ab5a240d52a94314c963476c989814ba9b519bc842e5b61b464bb", 138 | "sha256:c70c7dd733a4c56838d1f1781e769081a25fade879510c5b5f0df76956abfa05", 139 | "sha256:c767458511a59f6f597bfb0032a1c82a52c29ae228c2c0a6865cfeaeaac4c5f5", 140 | "sha256:c87df8ae3f01ffb4483c796fe1b15232ce2b219f0b18126948616224d3f658ee", 141 | "sha256:ca1c4a569232c063615f9e70ff9a1e2fee8c66a6fb5caf0f5e8b21a396deec3e", 142 | "sha256:cc407b68e0a874e7ece60f6639df46309376882152345508be94da608cc0b831", 143 | "sha256:da862b8f7de577bc421323714f63276acb2f759ab8c5e33335509f0b89e06b8f", 144 | "sha256:dfe7eac0d253915116ed0cd160a15a88981a1d194c1ef151e862a5c7d2f853d3", 145 | "sha256:ed1377feed808c9c1139bdb6a61bcbf030c236dd288d6fca71ac26906ab03ba6", 146 | "sha256:f42ad188466d946f1b3afc0a9e1a266ac8926461ee0786c06baac6bd71f8a6f3", 147 | "sha256:f92731609d6625e1cc26ff5757db4d32b6b810d2a3363b0ff94ff573e5901f6f" 148 | ], 149 | "markers": "python_version >= '3'", 150 | "version": "==1.1.0" 151 | }, 152 | "idna": { 153 | "hashes": [ 154 | "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", 155 | "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" 156 | ], 157 | "markers": "python_version >= '3'", 158 | "version": "==3.2" 159 | }, 160 | "importlib-metadata": { 161 | "hashes": [ 162 | "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac", 163 | "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e" 164 | ], 165 | "markers": "python_version < '3.8'", 166 | "version": "==4.6.1" 167 | }, 168 | "itsdangerous": { 169 | "hashes": [ 170 | "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", 171 | "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" 172 | ], 173 | "markers": "python_version >= '3.6'", 174 | "version": "==2.0.1" 175 | }, 176 | "jinja2": { 177 | "hashes": [ 178 | "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", 179 | "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" 180 | ], 181 | "markers": "python_version >= '3.6'", 182 | "version": "==3.0.1" 183 | }, 184 | "mako": { 185 | "hashes": [ 186 | "sha256:17831f0b7087c313c0ffae2bcbbd3c1d5ba9eeac9c38f2eb7b50e8c99fe9d5ab", 187 | "sha256:aea166356da44b9b830c8023cd9b557fa856bd8b4035d6de771ca027dfc5cc6e" 188 | ], 189 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 190 | "version": "==1.1.4" 191 | }, 192 | "markupsafe": { 193 | "hashes": [ 194 | "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", 195 | "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", 196 | "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", 197 | "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", 198 | "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", 199 | "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", 200 | "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", 201 | "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", 202 | "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", 203 | "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", 204 | "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", 205 | "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", 206 | "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", 207 | "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", 208 | "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", 209 | "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", 210 | "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", 211 | "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", 212 | "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", 213 | "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", 214 | "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", 215 | "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", 216 | "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", 217 | "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", 218 | "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", 219 | "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", 220 | "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", 221 | "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", 222 | "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", 223 | "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", 224 | "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", 225 | "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", 226 | "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", 227 | "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" 228 | ], 229 | "markers": "python_version >= '3.6'", 230 | "version": "==2.0.1" 231 | }, 232 | "pymysql": { 233 | "hashes": [ 234 | "sha256:41fc3a0c5013d5f039639442321185532e3e2c8924687abe6537de157d403641", 235 | "sha256:816927a350f38d56072aeca5dfb10221fe1dc653745853d30a216637f5d7ad36" 236 | ], 237 | "index": "pypi", 238 | "version": "==1.0.2" 239 | }, 240 | "python-dateutil": { 241 | "hashes": [ 242 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 243 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 244 | ], 245 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 246 | "version": "==2.8.2" 247 | }, 248 | "python-dotenv": { 249 | "hashes": [ 250 | "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1", 251 | "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172" 252 | ], 253 | "index": "pypi", 254 | "version": "==0.19.0" 255 | }, 256 | "python-editor": { 257 | "hashes": [ 258 | "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", 259 | "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", 260 | "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8" 261 | ], 262 | "version": "==1.0.4" 263 | }, 264 | "pytz": { 265 | "hashes": [ 266 | "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", 267 | "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" 268 | ], 269 | "version": "==2021.1" 270 | }, 271 | "requests": { 272 | "hashes": [ 273 | "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", 274 | "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" 275 | ], 276 | "index": "pypi", 277 | "version": "==2.26.0" 278 | }, 279 | "six": { 280 | "hashes": [ 281 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 282 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 283 | ], 284 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 285 | "version": "==1.16.0" 286 | }, 287 | "sqlalchemy": { 288 | "hashes": [ 289 | "sha256:09dbb4bc01a734ccddbf188deb2a69aede4b3c153a72b6d5c6900be7fb2945b1", 290 | "sha256:12bac5fa1a6ea870bdccb96fe01610641dd44ebe001ed91ef7fcd980e9702db5", 291 | "sha256:1fdae7d980a2fa617d119d0dc13ecb5c23cc63a8b04ffcb5298f2c59d86851e9", 292 | "sha256:26daa429f039e29b1e523bf763bfab17490556b974c77b5ca7acb545b9230e9a", 293 | "sha256:36a089dc604032d41343d86290ce85d4e6886012eea73faa88001260abf5ff81", 294 | "sha256:39b5d36ab71f73c068cdcf70c38075511de73616e6c7fdd112d6268c2704d9f5", 295 | "sha256:4014978de28163cd8027434916a92d0f5bb1a3a38dff5e8bf8bff4d9372a9117", 296 | "sha256:44d23ea797a5e0be71bc5454b9ae99158ea0edc79e2393c6e9a2354de88329c0", 297 | "sha256:488608953385d6c127d2dcbc4b11f8d7f2f30b89f6bd27c01b042253d985cc2f", 298 | "sha256:5102b9face693e8b2db3b2539c7e1a5d9a5b4dc0d79967670626ffd2f710d6e6", 299 | "sha256:5908ea6c652a050d768580d01219c98c071e71910ab8e7b42c02af4010608397", 300 | "sha256:5d856cc50fd26fc8dd04892ed5a5a3d7eeb914fea2c2e484183e2d84c14926e0", 301 | "sha256:68393d3fd31469845b6ba11f5b4209edbea0b58506be0e077aafbf9aa2e21e11", 302 | "sha256:6a16c7c4452293da5143afa3056680db2d187b380b3ef4d470d4e29885720de3", 303 | "sha256:756f5d2f5b92d27450167247fb574b09c4cd192a3f8c2e493b3e518a204ee543", 304 | "sha256:891927a49b2363a4199763a9d436d97b0b42c65922a4ea09025600b81a00d17e", 305 | "sha256:9bfe882d5a1bbde0245dca0bd48da0976bd6634cf2041d2fdf0417c5463e40e5", 306 | "sha256:9fcbb4b4756b250ed19adc5e28c005b8ed56fdb5c21efa24c6822c0575b4964d", 307 | "sha256:a00d9c6d3a8afe1d1681cd8a5266d2f0ed684b0b44bada2ca82403b9e8b25d39", 308 | "sha256:a5e14cb0c0a4ac095395f24575a0e7ab5d1be27f5f9347f1762f21505e3ba9f1", 309 | "sha256:b48148ceedfb55f764562e04c00539bb9ea72bf07820ca15a594a9a049ff6b0e", 310 | "sha256:b7fb937c720847879c7402fe300cfdb2aeff22349fa4ea3651bca4e2d6555939", 311 | "sha256:bc34a007e604091ca3a4a057525efc4cefd2b7fe970f44d20b9cfa109ab1bddb", 312 | "sha256:c9373ef67a127799027091fa53449125351a8c943ddaa97bec4e99271dbb21f4", 313 | "sha256:d09a760b0a045b4d799102ae7965b5491ccf102123f14b2a8cc6c01d1021a2d9", 314 | "sha256:ec1be26cdccd60d180359a527d5980d959a26269a2c7b1b327a1eea0cab37ed8", 315 | "sha256:eedd76f135461cf237534a6dc0d1e0f6bb88a1dc193678fab48a11d223462da5", 316 | "sha256:f028ef6a1d828bc754852a022b2160e036202ac8658a6c7d34875aafd14a9a15", 317 | "sha256:f814d80844969b0d22ea63663da4de5ca1c434cfbae226188901e5d368792c17", 318 | "sha256:fd2102a8f8a659522719ed73865dff3d3cc76eb0833039dc473e0ad3041d04be" 319 | ], 320 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 321 | "version": "==1.4.22" 322 | }, 323 | "typing-extensions": { 324 | "hashes": [ 325 | "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", 326 | "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", 327 | "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" 328 | ], 329 | "markers": "python_version < '3.8'", 330 | "version": "==3.10.0.0" 331 | }, 332 | "tzlocal": { 333 | "hashes": [ 334 | "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44", 335 | "sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4" 336 | ], 337 | "version": "==2.1" 338 | }, 339 | "urllib3": { 340 | "hashes": [ 341 | "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", 342 | "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" 343 | ], 344 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 345 | "version": "==1.26.6" 346 | }, 347 | "werkzeug": { 348 | "hashes": [ 349 | "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42", 350 | "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8" 351 | ], 352 | "markers": "python_version >= '3.6'", 353 | "version": "==2.0.1" 354 | }, 355 | "zipp": { 356 | "hashes": [ 357 | "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", 358 | "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" 359 | ], 360 | "markers": "python_version >= '3.6'", 361 | "version": "==3.5.0" 362 | } 363 | }, 364 | "develop": {} 365 | } 366 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flask-scaffolding 2 | ![](docs/image/flask-deploy.png) 3 | flask项目脚手架,可以基于此脚手架快速开发,减少重复操作,目前已经提供的能力: 4 | - vscode作为开发IDE 5 | - [pipenv](https://pipenv.pypa.io/en/latest/)管理开发环境 6 | - 基于docker的nginx+gunicorn+supervisor一键部署 7 | - migrations管理数据库迁移 8 | - dotenv管理环境变量敏感信息 9 | - 日志(支持在线预览) 10 | - 开发/线上配置Config 11 | - 规范的flask目录结构 12 | 13 | 这里有一个基于flask-scaffolding开发的微信公众号后台项目[werobot](https://github.com/barry-ran/werobot) 14 | 15 | 扫码下面二维码即可体验: 16 | 17 | ![](https://github.com/barry-ran/werobot/blob/main/docs/image/coderbox.jpg) 18 | 19 | 使用步骤: 20 | 1. 根据文档描述配置开发环境 21 | 2. 根据需要初始化数据库(部署会尝试自动初始化数据库) 22 | 3. 开发/部署 23 | 24 | 下面是demo展示: 25 | ![](docs/image/screenshot.png) 26 | # 开发环境配置 27 | ## vscode作为开发IDE 28 | 1. 安装[python插件](https://code.visualstudio.com/docs/python/python-tutorial) 29 | 30 | 2. 在左下角切换python环境为pipenv创建的环境: 31 | ![](docs/image/select-pipenv.png) 32 | 3. 调试运行:在左侧调试tab里可以通过Run and Debug来调试 33 | ![](docs/image/run-debug1.png) 34 | 以flask方式运行时,需要指定我们flask app所在的python文件 35 | ![](docs/image/run-debug2.png) 36 | 不想每次指定要运行的python文件可以create a launch.json来设置启动参数,注意环境指定: 37 | - FLASK_APP中指定的是业务逻辑上的环境(例如不同环境的数据库配置) 38 | - FLASK_ENV指定的是flask的环境(development环境会默认开启DEBUG模式),flask 1.0以后只能使用环境变量的方式配置环境 39 | DEBUG模式默认会启动debugger(发生错误时直接在网页中显示代码错误信息,并且可以直接shell调试),可以通过args中添加"--no-debugger"来关闭debugger 40 | ``` 41 | { 42 | // Use IntelliSense to learn about possible attributes. 43 | // Hover to view descriptions of existing attributes. 44 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 45 | "version": "0.2.0", 46 | "configurations": [ 47 | { 48 | "name": "Python: Flask", 49 | "type": "python", 50 | "request": "launch", 51 | "module": "flask", 52 | "env": { 53 | "FLASK_APP": "app:create_app('development')", 54 | "FLASK_ENV": "development" 55 | }, 56 | "args": [ 57 | "run" 58 | ], 59 | "jinja": true 60 | } 61 | ] 62 | } 63 | ``` 64 | 4. 运行单个python文件:可以在python文件右上角直接run python file in terminal 65 | ![](docs/image/run-python.png) 66 | ## 使用[pipenv](https://pipenv.pypa.io/en/latest/)管理依赖 67 | ``` 68 | pipenv install # 创建虚拟环境并安装依赖(只有第一次搭建环境需要安装) 69 | pipenv shell # 激活虚拟环境 70 | pipenv lock -r > ./app/requirements.txt # 导出所有依赖到requirements 71 | pip freeze > requirements.txt # 导出所有依赖到requirements (两种方法都可以) 72 | ``` 73 | 74 | ## 数据库 75 | ### docker安装mysql(可选) 76 | - 构建docker镜像 77 | ``` 78 | # 如果没有本地image则会自动下载 79 | docker run --name mysql5.7 -p 4418:3306 -v ~/conf:/etc/mysql/conf.d -v ~/logs:/logs -v ~/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=mysql123465 -d mysql/mysql-server:5.7 80 | ``` 81 | - 增加新用户 82 | ``` 83 | docker exec -it mysql5.7 bash 84 | mysql -uroot -pmysql123465 85 | use mysql; 86 | grant all privileges on *.* to werobot@'%' identified by "mysql123465"; 87 | flush privileges; 88 | ``` 89 | - 创建数据库 90 | ``` 91 | create database werobot; 92 | ``` 93 | 94 | ### 初始化数据库 95 | 使用[Flask-Migrate](https://github.com/miguelgrinberg/Flask-Migrate)管理数据库 96 | - 第一次配置开发环境需要先执行以下命令初始化迁移文件,并生成表 97 | ```bash 98 | # 默认在当前目录生成迁移文件夹migrations,可以通过-d选项来指定不同路径 99 | # 如果设定了其它路径,后续命令都需要使用-d指定路径 100 | flask db init # 第一次初始化数据库升级环境(生成migrations目录,需要提交到代码仓库中纳入版本管理) 101 | flask db migrate -m "Initial migration." # 更新数据库结构到migrations(-m可选) 102 | flask db upgrade # 更新实际数据库(将migrations修改应用到实际数据库中,第一次执行会创建表) 103 | ``` 104 | - 后续开发过程中更改表结构只需要执行以下命令即可更新数据库 105 | ``` 106 | flask db migrate 107 | flask db upgrade 108 | ``` 109 | 110 | # 部署 111 | 基于docker的nginx+gunicorn+supervisor部署: 112 | - gunicorn:开启多进程基于gevent为flask提供wsgi服务 113 | - nginx:反向代理,高效处理静态资源 114 | - supervisor:进程管理,监控并自动重启nginx和gunicorn 115 | 116 | 安装好docker以后clone下代码来可以一键部署: 117 | ```bash 118 | # 不带-d,调试用 119 | # docker-compose up 120 | docker-compose up -d 121 | 122 | # 也可以直接执行提供的script来部署 123 | ./script/compose-deploy.sh 124 | ``` 125 | 126 | 配置文件说明: 127 | - gunicorn.conf.py:gunicorn配置文件:配置usgi服务的进程数,端口号等 128 | - nginx_flask.conf:nginx配置文件:配置nginx反向代理的端口号等 129 | - supervisord.conf:supervisor配置文件:配置supervisor如何监控&启动gunicorn和nginx 130 | - Dockerfile:docker image配置文件,用于docker部署 131 | - docker-compose.yaml:docker-compose配置文件,用于方便的部署docker 132 | - requirements.txt:python依赖项,用于docker部署时安装依赖,一般开发完增加了依赖项的话需要重新生产该文件 133 | - Pipfile:pipenv依赖配置,用于开发阶段的环境配置 134 | 135 | ## [安装docker](https://www.runoob.com/docker/centos-docker-install.html) 136 | Docker支持以下的CentOS版本: 137 | 138 | - CentOS 7 (64-bit) 139 | - CentOS 6.5 (64-bit) 或更高的版本 140 | 141 | ### 前提条件 142 | 目前,CentOS 仅发行版本中的内核支持 Docker。 143 | 144 | Docker 运行在 CentOS 7 上,要求系统为64位、系统内核版本为 3.10 以上。 145 | 146 | Docker 运行在 CentOS-6.5 或更高的版本的 CentOS 上,要求系统为64位、系统内核版本为 2.6.32-431 或者更高版本。 147 | 148 | ### 使用 yum 安装(CentOS 7下) 149 | Docker 要求 CentOS 系统的内核版本高于 3.10 ,查看本页面的前提条件来验证你的CentOS 版本是否支持 Docker 。 150 | 151 | 通过 uname -r 命令查看你当前的内核版本 152 | ``` 153 | uname -r 154 | ``` 155 | 156 | ### 安装 Docker 157 | 移除旧的版本: 158 | ``` 159 | sudo yum remove docker \ 160 | docker-client \ 161 | docker-client-latest \ 162 | docker-common \ 163 | docker-latest \ 164 | docker-latest-logrotate \ 165 | docker-logrotate \ 166 | docker-selinux \ 167 | docker-engine-selinux \ 168 | docker-engine 169 | ``` 170 | 171 | 安装一些必要的系统工具: 172 | ``` 173 | sudo yum install -y yum-utils device-mapper-persistent-data lvm2 174 | ``` 175 | 176 | 添加软件源信息: 177 | ``` 178 | sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 179 | ``` 180 | 181 | 更新 yum 缓存: 182 | ``` 183 | sudo yum makecache fast 184 | ``` 185 | 186 | 安装 Docker-ce: 187 | ``` 188 | sudo yum -y install docker-ce 189 | ``` 190 | 191 | 启动 Docker 后台服务: 192 | ``` 193 | sudo systemctl start docker 194 | ``` 195 | 196 | ### 安装docker compose 197 | ```bash 198 | #国内访问github太慢,可以使用镜像站 github.com.cnpmjs.org 199 | #curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" > /usr/bin/docker-compose 200 | curl -L "https://github.com.cnpmjs.org/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" > /usr/bin/docker-compose 201 | 202 | chmod +x /usr/bin/docker-compose 203 | ``` 204 | 205 | # 常用命令 206 | ## pipenv 207 | ``` bash 208 | pipenv install # 创建虚拟环境并安装依赖(只有第一次搭建环境需要安装) 209 | pipenv shell # 激活虚拟环境 210 | pipenv lock -r > ./app/requirements.txt # 导出所有依赖到requirements 211 | pip freeze > requirements.txt # 导出所有依赖到requirements (两种方法都可以) 212 | ``` 213 | ## 数据库 214 | ```bash 215 | # 默认在当前目录生成迁移文件夹migrations,可以通过-d选项来指定不同路径 216 | # 如果设定了其它路径,后续命令都需要使用-d指定路径 217 | flask db init # 第一次初始化数据库升级环境(生成migrations目录,需要提交到代码仓库中纳入版本管理) 218 | flask db migrate -m "Initial migration." # 更新数据库结构到migrations(-m可选) 219 | flask db upgrade # 更新实际数据库(将migrations修改应用到实际数据库中,第一次执行会创建表) 220 | ``` 221 | 222 | ## docker 223 | ```bash 224 | # 列出正在运行的容器 225 | docker ps 226 | # 进入容器 227 | docker exec -it bash 228 | 229 | # 后台运行docker并且不退出(dit) 230 | docker run -dit -p 5000:80 --name=flask-scaffolding -v $(pwd)/app:/deploy/app flask-scaffolding 231 | 232 | # 安装vim 233 | apt-get update 234 | apt-get install vim 235 | ``` 236 | 237 | ## centos 238 | ```bash 239 | # 安装rzls 240 | yum -y install lrzsz 241 | # 安装unzip 242 | yum install zip unzip 243 | ``` 244 | 245 | # 参考文档 246 | ## 项目结构 247 | - [flask项目的结构](https://lepture.com/en/2018/structure-of-a-flask-project) 248 | - [Flask项目结构模板(主要参考这个)](https://www.justdopython.com/2020/01/18/python-web-flask-project-125/) 249 | - [Flask 从入门到放弃6: 网站结构最佳实践(来自狗书第七章:大型程序的结构)](https://lvraikkonen.github.io/2017/08/28/Flask%20%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E6%94%BE%E5%BC%836:%20%E7%BD%91%E7%AB%99%E7%BB%93%E6%9E%84%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/) 250 | 251 | ## 环境搭建 252 | - [python vscode环境搭建](https://zhuanlan.zhihu.com/p/64994681) 253 | 254 | ## 数据库 255 | - [使用 flask migrate 来迁移数据结构](https://einverne.github.io/post/2018/05/flask-migrate-tutorial.html) 256 | - [flask_sqlalchemy增删改查](https://blog.csdn.net/Co_zy/article/details/77937195) 257 | 258 | ## 日志 259 | - [flask写日志](https://blog.csdn.net/qq_36441027/article/details/111182467) 260 | - [flask错误处理](https://dormousehole.readthedocs.io/en/latest/errorhandling.html) 261 | 262 | ## 部署 263 | - [docker部署python](https://docs.docker.com/language/python/build-images/) 264 | - [Deploy flask app with nginx using gunicorn and supervisor](https://medium.com/ymedialabs-innovation/deploy-flask-app-with-nginx-using-gunicorn-and-supervisor-d7a93aa07c18) 265 | - [用docker部署flask+gunicorn+nginx](https://www.cnblogs.com/xuanmanstein/p/7692256.html) 266 | - [部署Flask + uWSGI + Nginx(支持https)](https://blog.zengrong.net/post/deploy-flask-uwsgi-nginx/) 267 | - [Flask后端实践 番外篇 Docker部署优化(docker-compose)](https://blog.csdn.net/qq_22034353/article/details/89950228) 268 | - [docker和docker compose版本匹配](https://docs.docker.com/compose/compose-file/) 269 | 270 | ## docker部署mysql 271 | - [使用Docker搭建MySQL服务](https://www.cnblogs.com/sablier/p/11605606.html) 272 | - [探索Docker容器下MySQL的数据持久化](https://www.dazhuanlan.com/2019/10/18/5da8dc453e9f9/) 273 | - [9个docker常见错误(发现了docker yml中不能访问volume映射的原因)](https://runnable.com/blog/9-common-dockerfile-mistakes) 274 | 275 | ## 其他 276 | - [使用dotnev加载环境变量](https://github.com/theskumar/python-dotenv#readme) 277 | - [flask处理ajax](https://blog.csdn.net/jinixin/article/details/80042763) 278 | - [flask favicon.ico 404(浏览器默认行为,需要我们在html中指定icon的路径)](https://stackoverflow.com/questions/48863061/favicon-ico-results-in-404-error-in-flask-app) 279 | - [Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first (chrome等浏览器不允许带声音的媒体自动播放)](https://developer.chrome.com/blog/autoplay/) 280 | - [demo源码参考](https://github.com/JMWpower/xiaojiejie) 281 | - [google-python命名规范](https://zh-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/python_style_rules/#id16) 282 | -------------------------------------------------------------------------------- /app/.env: -------------------------------------------------------------------------------- 1 | # 测试环境变量(.env文件一般保存私有信息,最好不要提交到仓库中,这里只是为了演示) 2 | SECRET_KEY=testkeyaabbcc -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from . import routes, models 3 | from .config import config 4 | from .log import Log 5 | 6 | # flask run命令会自动调用create_app函数 https://dormousehole.readthedocs.io/en/latest/cli.html 7 | # 通过flask run启动时,可以通过设置FLASK_APP=app:create_app('development')来指定create_app的参数 8 | def create_app(config_name='default'): 9 | app = Flask(__name__) 10 | app.config.from_object(config[config_name]) 11 | config[config_name].init_app(app) 12 | 13 | Log.init_app(app) 14 | 15 | models.init_app(app) 16 | routes.init_app(app) 17 | 18 | Log.logger().info('create_app:(%s)', config_name) 19 | 20 | return app -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | basedir = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | dotenv_path = os.path.join(basedir, '.env') 7 | load_dotenv(dotenv_path) 8 | 9 | class Config: 10 | # falsk的内置config key: https://flask.palletsprojects.com/en/2.0.x/config/ 11 | SECRET_KEY = os.environ.get('SECRET_KEY') or os.urandom(16) 12 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True 13 | SQLALCHEMY_TRACK_MODIFICATIONS = True 14 | 15 | PRODUCTION_CONFIG = False 16 | 17 | # init_app是为了在初始化app时附加一些额外配置用的 18 | @classmethod 19 | def init_app(cls, app): 20 | pass 21 | 22 | class DevelopmentConfig(Config): 23 | # 设置了FLASK_ENV环境变量自动是DEBUG模式 24 | # DEBUG = True 25 | # 没有指定DEV_DATABASE_URL则使用sqlite 26 | SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or 'sqlite:///' + os.path.join(basedir, 'data.db') 27 | # 查询时会显示原始SQL语句 28 | SQLALCHEMY_ECHO= True 29 | PRODUCTION_CONFIG = False 30 | 31 | class ProductionConfig(Config): 32 | # 没有指定DATABASE_URL则使用sqlite 33 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///' + os.path.join(basedir, 'data.db') 34 | PRODUCTION_CONFIG = True 35 | 36 | config = { 37 | 'development': DevelopmentConfig, 38 | 'production': ProductionConfig, 39 | 'default': DevelopmentConfig 40 | } -------------------------------------------------------------------------------- /app/log.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from logging import handlers 4 | from werkzeug.exceptions import InternalServerError 5 | 6 | basedir = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | def handle_error(error): 9 | Log.logger().error(error) 10 | return error 11 | 12 | class Log: 13 | LOG_PATH = os.path.join(basedir, 'logs') 14 | LOG_NAME = os.path.join(LOG_PATH, 'log.txt') 15 | LOG_LEVEL = 'INFO' 16 | 17 | current_app = None 18 | 19 | @staticmethod 20 | def init_app(app): 21 | Log.current_app = app 22 | if not os.path.exists(Log.LOG_PATH): 23 | os.makedirs(Log.LOG_PATH) 24 | 25 | # 根据时间重命名log 26 | file_handler = logging.handlers.TimedRotatingFileHandler(Log.LOG_NAME, when='D', interval=1, backupCount=0, encoding='utf-8') 27 | file_handler.suffix = '%Y-%m-%d.log' 28 | # 单独设置handler的日志级别:低于该级别则该handler不处理(一个logger可以有多个handler) 29 | # file_handler用来写入文件 30 | file_handler.setLevel(Log.LOG_LEVEL) 31 | 32 | fmt = '%(asctime)s-%(levelname)s-%(filename)s-%(funcName)s-%(lineno)s: %(message)s' 33 | formatter = logging.Formatter(fmt) 34 | file_handler.setFormatter(formatter) 35 | 36 | # 设置logger的日志级别:大于等于该级别才会交给handler处理 37 | app.logger.setLevel('DEBUG') 38 | app.logger.addHandler(file_handler) 39 | 40 | # DEBUG模式下不会走到handle_error 41 | app.register_error_handler(InternalServerError, handle_error) 42 | 43 | @staticmethod 44 | def logger(): 45 | return Log.current_app.logger -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | 3 | # 用flask run命令启动app/main时会自动探测到我们创建的这个app对象来启动服务器 4 | # 更多flask run的知识可以看这里:https://dormousehole.readthedocs.io/en/latest/cli.html 5 | app = Flask(__name__) 6 | 7 | @app.route("/") 8 | def index(): 9 | return render_template('index.html') 10 | 11 | if __name__ == "__main__": 12 | # 仅仅直接python main.py运行的时候才会通过这里启动服务器 13 | app.run(port=5000, debug=True) -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from flask_sqlalchemy import SQLAlchemy 3 | from flask_migrate import Migrate 4 | 5 | db = SQLAlchemy() 6 | 7 | def init_app(app): 8 | migrate = Migrate(app, db) 9 | db.init_app(app) 10 | migrate.init_app(app) 11 | 12 | return db -------------------------------------------------------------------------------- /app/models/ip_count.py: -------------------------------------------------------------------------------- 1 | from . import db 2 | 3 | # https://dormousehole.readthedocs.io/en/latest/patterns/sqlalchemy.html 4 | # 增删改查 https://blog.csdn.net/Co_zy/article/details/77937195 5 | 6 | class IpCount(db.Model): 7 | __tablename__ = 'ipcount' 8 | id = db.Column(db.Integer, primary_key = True) 9 | ip = db.Column(db.String(256), index = True, unique = True) 10 | count = db.Column(db.Integer, index = True, default=0) 11 | 12 | def __init__(self, ip=None, count=None): 13 | self.ip = ip 14 | self.count = count 15 | 16 | @classmethod 17 | def get_count(cls, ip): 18 | count = cls.query.filter(IpCount.ip==ip).first() 19 | if None == count: 20 | return 0 21 | 22 | return count.count 23 | 24 | @classmethod 25 | def set_count(cls, ip, count): 26 | 27 | ipCount = cls.query.filter(IpCount.ip==ip).first() 28 | if None == ipCount: 29 | ipCount = IpCount(ip, count) 30 | db.session.add(ipCount) 31 | db.session.commit() 32 | 33 | ipCount.count = count 34 | db.session.commit() -------------------------------------------------------------------------------- /app/routes/__init__.py: -------------------------------------------------------------------------------- 1 | from .home import home_bp 2 | from .log_list import log_list_bp 3 | from .girls import girls_bp 4 | 5 | def init_app(app): 6 | app.register_blueprint(home_bp) 7 | app.register_blueprint(log_list_bp) 8 | app.register_blueprint(girls_bp) -------------------------------------------------------------------------------- /app/routes/girls.py: -------------------------------------------------------------------------------- 1 | import random 2 | import requests 3 | from flask import Blueprint, request 4 | from ..log import Log 5 | 6 | girls_bp = Blueprint('girls_bp', __name__) 7 | 8 | @girls_bp.route('/girls', methods=['GET', 'POST']) 9 | def girls(): 10 | Log.logger().info('recv girls request type:%s', request.method) 11 | # GET上传的数据用request.args获取,POST上传的数据用request.form获取 12 | if request.method == 'GET': 13 | data = request.args 14 | else: 15 | data = request.form 16 | 17 | Log.logger().info('recv girls request data:%s', data) 18 | return get_girls() 19 | 20 | def get_girls(): 21 | try: 22 | t = random.randint(10000000000000000, 99999999999999999) 23 | url = 'http://wmsp.cc/video.php?_t=0.{}'.format(t) 24 | header = { 25 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:21.0) Gecko/20100101 Firefox/21.0' 26 | } 27 | Log.logger().info('request girls url:%s', url) 28 | resp = requests.get(url, headers=header, allow_redirects=False) 29 | Log.logger().info('request girls resp:%s', resp.headers) 30 | return resp.headers['Location'] 31 | except Exception as e: 32 | Log.logger().error('failed request girls:%s', e) 33 | return './demo.mp4' -------------------------------------------------------------------------------- /app/routes/home.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Blueprint, render_template, request, current_app 3 | from ..models.ip_count import IpCount 4 | from ..log import Log 5 | 6 | home_bp = Blueprint('home_bp', __name__) 7 | 8 | # 如果需要多个路径路由到通过一个函数,直接在此添加即可 9 | # @home_bp.route('/index.html', methods=['GET', 'POST']) 10 | @home_bp.route('/', methods=['GET', 'POST']) 11 | def index(): 12 | ip = request.remote_addr 13 | Log.logger().info('index request.remote_addr:%s', ip) 14 | 15 | if 'X-Forwarded-For' in request.headers: 16 | ip = request.headers['X-Forwarded-For'] 17 | Log.logger().info("index request.headers['X-Forwarded-For']:%s", ip) 18 | 19 | count = IpCount.get_count(ip) 20 | count = count + 1 21 | IpCount.set_count(ip, count) 22 | 23 | Log.logger().info('index ip:%s count:%d', ip, count) 24 | 25 | if not current_app.config['PRODUCTION_CONFIG']: 26 | Log.logger().info("SECRET_KEY:%s", os.environ.get('SECRET_KEY')) 27 | 28 | return render_template('index.html', ip_count=count) -------------------------------------------------------------------------------- /app/routes/log_list.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Blueprint, render_template, send_from_directory, current_app 3 | from ..log import Log 4 | 5 | # https://blog.csdn.net/kaever/article/details/116312794?spm=1001.2014.3001.5501 6 | 7 | log_list_bp = Blueprint('log_list_bp', __name__) 8 | 9 | basedir = os.path.abspath(os.path.dirname(__file__)) 10 | logs_dir = os.path.join(basedir, '../logs') 11 | 12 | @log_list_bp.route('/logs', methods=['GET', 'POST']) 13 | def log_list(): 14 | if current_app.config['PRODUCTION_CONFIG']: 15 | return 'no log on production' 16 | 17 | names = os.listdir(logs_dir) 18 | files = {} 19 | for name in names: 20 | path = os.path.join('/logs', name) 21 | Log.logger().info("list log file:%s", path) 22 | files.update({name:path}) 23 | 24 | return render_template('log_list.html', files=files) 25 | 26 | @log_list_bp.route('/logs/') 27 | def download(filename): 28 | Log.logger().info("send file:%s:%s", logs_dir, filename) 29 | return send_from_directory(logs_dir, filename) -------------------------------------------------------------------------------- /app/static/css/pc.css: -------------------------------------------------------------------------------- 1 | *{ 2 | margin:0px; 3 | padding:0px; 4 | touch-action: pan-y; 5 | background-color: #161823; 6 | } 7 | 8 | .app{ 9 | width: 650px; 10 | height:640px; 11 | position: absolute; 12 | top:50px; 13 | right:0px; 14 | bottom: 0px; 15 | left:0px; 16 | overflow: hidden; 17 | margin:0 auto; 18 | background-color: #0E0F1A; 19 | } 20 | 21 | .bom{ 22 | position: absolute; 23 | top:730px; 24 | right:0px; 25 | bottom: 0px; 26 | left:0px; 27 | overflow: hidden; 28 | margin:0 auto; 29 | font-size:12px; 30 | } 31 | 32 | #player{ 33 | width: 348px; 34 | height: 600px; 35 | margin-top: 20px; 36 | margin-bottom: 20px; 37 | margin-left: 20px; 38 | float:left; 39 | z-index: 100; 40 | } 41 | 42 | .box{ 43 | width: 242px; 44 | height: 600px; 45 | margin-top: 20px; 46 | margin-bottom: 20px; 47 | margin-left: 20px; 48 | background-color: #0E0F1A; 49 | float:left; 50 | font-size:12px; 51 | } 52 | 53 | .xhms { 54 | -moz-box-shadow:inset 0px 1px 0px 0px #f5978e; 55 | -webkit-box-shadow:inset 0px 1px 0px 0px #f5978e; 56 | box-shadow:inset 0px 1px 0px 0px #f5978e; 57 | background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #f24537), color-stop(1, #c62d1f) ); 58 | background:-moz-linear-gradient( center top, #f24537 5%, #c62d1f 100% ); 59 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f24537', endColorstr='#c62d1f'); 60 | background-color:#f24537; 61 | -webkit-border-top-left-radius:5px; 62 | -moz-border-radius-topleft:5px; 63 | border-top-left-radius:5px; 64 | -webkit-border-top-right-radius:5px; 65 | -moz-border-radius-topright:5px; 66 | border-top-right-radius:5px; 67 | -webkit-border-bottom-right-radius:5px; 68 | -moz-border-radius-bottomright:5px; 69 | border-bottom-right-radius:5px; 70 | -webkit-border-bottom-left-radius:5px; 71 | -moz-border-radius-bottomleft:5px; 72 | border-bottom-left-radius:5px; 73 | text-indent:0; 74 | display:inline-block; 75 | color:#ffffff; 76 | font-family:arial; 77 | font-size:15px; 78 | font-weight:bold; 79 | font-style:italic; 80 | height:30px; 81 | line-height:30px; 82 | width:160px; 83 | text-decoration:none; 84 | text-align:center; 85 | text-shadow:0px 1px 0px #810e05; 86 | }.xhms:hover { 87 | background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #c62d1f), color-stop(1, #f24537) ); 88 | background:-moz-linear-gradient( center top, #c62d1f 5%, #f24537 100% ); 89 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#c62d1f', endColorstr='#f24537'); 90 | background-color:#c62d1f; 91 | }.xhms:active { 92 | position:relative; 93 | top:1px; 94 | } 95 | 96 | .zdlb { 97 | -moz-box-shadow:inset 0px 1px 0px 0px #a4e271; 98 | -webkit-box-shadow:inset 0px 1px 0px 0px #a4e271; 99 | box-shadow:inset 0px 1px 0px 0px #a4e271; 100 | background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #89c403), color-stop(1, #77a809) ); 101 | background:-moz-linear-gradient( center top, #89c403 5%, #77a809 100% ); 102 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#89c403', endColorstr='#77a809'); 103 | background-color:#89c403; 104 | -webkit-border-top-left-radius:5px; 105 | -moz-border-radius-topleft:5px; 106 | border-top-left-radius:5px; 107 | -webkit-border-top-right-radius:5px; 108 | -moz-border-radius-topright:5px; 109 | border-top-right-radius:5px; 110 | -webkit-border-bottom-right-radius:5px; 111 | -moz-border-radius-bottomright:5px; 112 | border-bottom-right-radius:5px; 113 | -webkit-border-bottom-left-radius:5px; 114 | -moz-border-radius-bottomleft:5px; 115 | border-bottom-left-radius:5px; 116 | text-indent:0; 117 | display:inline-block; 118 | color:#ffffff; 119 | font-family:arial; 120 | font-size:15px; 121 | font-weight:bold; 122 | font-style:italic; 123 | height:30px; 124 | line-height:30px; 125 | width:160px; 126 | text-decoration:none; 127 | text-align:center; 128 | text-shadow:0px 1px 0px #528009; 129 | }.zdlb:hover { 130 | background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #77a809), color-stop(1, #89c403) ); 131 | background:-moz-linear-gradient( center top, #77a809 5%, #89c403 100% ); 132 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#77a809', endColorstr='#89c403'); 133 | background-color:#77a809; 134 | }.zdlb:active { 135 | position:relative; 136 | top:1px; 137 | } 138 | 139 | .pass { 140 | -moz-box-shadow:inset 0px 1px 0px 0px #ffffff; 141 | -webkit-box-shadow:inset 0px 1px 0px 0px #ffffff; 142 | box-shadow:inset 0px 1px 0px 0px #ffffff; 143 | background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #ededed), color-stop(1, #dfdfdf) ); 144 | background:-moz-linear-gradient( center top, #ededed 5%, #dfdfdf 100% ); 145 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ededed', endColorstr='#dfdfdf'); 146 | background-color:#ededed; 147 | -webkit-border-top-left-radius:5px; 148 | -moz-border-radius-topleft:5px; 149 | border-top-left-radius:5px; 150 | -webkit-border-top-right-radius:5px; 151 | -moz-border-radius-topright:5px; 152 | border-top-right-radius:5px; 153 | -webkit-border-bottom-right-radius:5px; 154 | -moz-border-radius-bottomright:5px; 155 | border-bottom-right-radius:5px; 156 | -webkit-border-bottom-left-radius:5px; 157 | -moz-border-radius-bottomleft:5px; 158 | border-bottom-left-radius:5px; 159 | text-indent:0; 160 | display:inline-block; 161 | color:#777777; 162 | font-family:arial; 163 | font-size:40px; 164 | font-weight:bold; 165 | font-style:italic; 166 | height:50px; 167 | line-height:50px; 168 | width:190px; 169 | text-decoration:none; 170 | text-align:center; 171 | text-shadow:0px 1px 0px #ffffff; 172 | }.pass:hover { 173 | background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #dfdfdf), color-stop(1, #ededed) ); 174 | background:-moz-linear-gradient( center top, #dfdfdf 5%, #ededed 100% ); 175 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#dfdfdf', endColorstr='#ededed'); 176 | background-color:#dfdfdf; 177 | }.pass:active { 178 | position:relative; 179 | top:1px; 180 | } 181 | 182 | #yuan{ 183 | line-height:26px; 184 | height:26px; 185 | width:182px; 186 | color:#1b1919; 187 | background-color:#ededed; 188 | font-size:17px; 189 | font-weight:bold; 190 | font-family:Arial; 191 | background:-webkit-gradient(linear, left top, left bottom, color-start(0.05, #ffec64), color-stop(1, #ffab23)); 192 | background:-moz-linear-gradient(top, #ffec64 5%, #ffab23 100%); 193 | background:-o-linear-gradient(top, #ffec64 5%, #ffab23 100%); 194 | background:-ms-linear-gradient(top, #ffec64 5%, #ffab23 100%); 195 | background:linear-gradient(to bottom, #ffec64 5%, #ffab23 100%); 196 | background:-webkit-linear-gradient(top, #ffec64 5%, #ffab23 100%); 197 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffec64', endColorstr='#ffab23',GradientType=0); 198 | border:3px solid #ffaa22; 199 | -webkit-border-top-left-radius:26px; 200 | -moz-border-radius-topleft:26px; 201 | border-top-left-radius:26px; 202 | -webkit-border-top-right-radius:26px; 203 | -moz-border-radius-topright:26px; 204 | border-top-right-radius:26px; 205 | -webkit-border-bottom-left-radius:26px; 206 | -moz-border-radius-bottomleft:26px; 207 | border-bottom-left-radius:26px; 208 | -webkit-border-bottom-right-radius:26px; 209 | -moz-border-radius-bottomright:26px; 210 | border-bottom-right-radius:26px; 211 | -moz-box-shadow: inset 0px 1px 20px -1px #fff6af; 212 | -webkit-box-shadow: inset 0px 1px 20px -1px #fff6af; 213 | box-shadow: inset 0px 1px 20px -1px #fff6af; 214 | text-align:center; 215 | display:inline-block; 216 | text-decoration:none; 217 | } 218 | #yuan:hover{ 219 | background-color:#f5f5f5; 220 | background:-webkit-gradient(linear, left top, left bottom, color-start(0.05, #ffab23), color-stop(1, #ffec64)); 221 | background:-moz-linear-gradient(top, #ffab23 5%, #ffec64 100%); 222 | background:-o-linear-gradient(top, #ffab23 5%, #ffec64 100%); 223 | background:-ms-linear-gradient(top, #ffab23 5%, #ffec64 100%); 224 | background:linear-gradient(to bottom, #ffab23 5%, #ffec64 100%); 225 | background:-webkit-linear-gradient(top, #ffab23 5%, #ffec64 100%); 226 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffab23', endColorstr='#ffec64',GradientType=0); 227 | } 228 | /* login */ 229 | .login-header{width:100%;text-align:center;height:30px;font-size:24px;line-height:30px;background-color:#ffffff;} 230 | .login{width:500px;position:fixed;border:#ebebeb solid 1px;top:50%;left:50%;display:none;background-color:#ffffff;box-shadow:0px 0px 20px #ddd;z-index:9999;margin-left:-250px;margin-top:-140px;} 231 | .login-title{width:100%;margin:10px 0px 0px 0px;text-align:center;line-height:40px;height:40px;font-size:18px;position:relative;background-color:#ffffff;} 232 | .login-title span{position:absolute;font-size:12px;right:-20px;top:-30px;border:#ebebeb solid 1px;width:40px;height:40px;border-radius:20px;background-color:#ffffff;} 233 | .login-title span a{display:block;border-radius:20px;font-size:16px;background-color:#ffffff;} 234 | .login strong{display:block;border-radius:20px;font-size:16px;background-color:#ffffff;} 235 | .login span{display:block;border-radius:20px;font-size:14px;background-color:#ffffff;} 236 | .login-input-content{margin-top:20px;background-color:#ffffff;} 237 | .login-input {overflow:hidden;margin:0px 0px 20px 0px;background-color:#ffffff;} 238 | .login-input label{float:left;width:90px;padding-right:10px;text-align:right;line-height:35px;height:35px;font-size:14px;background-color:#ffffff;} 239 | .login-input input.list-input{float:left;line-height:35px;height:35px;width:350px;border:#ebebeb 1px solid;text-indent:5px;background-color:#ffffff;} 240 | .login-button{width:50%;margin:30px auto 0px auto;line-height:40px;font-size:14px;border:#ebebeb 1px solid;text-align:center;background-color:#ffffff;} 241 | .login-button a{display:block;background-color:#ffffff;} 242 | .login-bg{width:100%;height:100%;position:fixed;top:0px;left:0px;background:#fff;filter:alpha(opacity=30);-moz-opacity:0.3;-khtml-opacity:0.3;opacity:0.3;display:none;} 243 | -------------------------------------------------------------------------------- /app/static/image/bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barry-ran/flask-scaffolding/4001614fa9d298a6c54438347ed5eca9c2c78ef8/app/static/image/bg.gif -------------------------------------------------------------------------------- /app/static/image/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barry-ran/flask-scaffolding/4001614fa9d298a6c54438347ed5eca9c2c78ef8/app/static/image/favicon.ico -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 19 | 20 | 随机小姐姐 21 | 22 | 23 | 28 | 29 | 30 | 31 | 随机小姐姐 32 | 33 |
34 |
35 | 38 |
39 |
40 |
41 | 本程序仅作研究学习演示,视频采集自网络;原作者拍摄创作不易,请勿下载传播! 43 |
44 |
46 | 这是你第{{ ip_count }}次访问本网站
47 | 点击按钮选择【连续】/【循环】模式
48 | 点击【PASS】按钮刷新播放下一个
49 | 单击视频 播放/暂停
50 | 双击视频进入 全屏播放
51 |
52 |
54 | 55 |
56 |
58 | 59 |
60 |
61 |
62 |
63 |
64 | 本程序仅作研究学习演示,数据采集自网络;原作者拍摄创作不易,请勿下载传播! 65 |
66 |
67 | 68 | 69 | 70 | 71 | 72 | 142 | 143 | -------------------------------------------------------------------------------- /app/templates/log_list.html: -------------------------------------------------------------------------------- 1 | 2 | Download 3 |

Directory listing

4 |
5 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | flask-scaffolding: 4 | image: flask-scaffolding:latest 5 | build: . 6 | container_name: flask-scaffolding 7 | restart: always 8 | ports: 9 | - "5000:80" 10 | volumes: 11 | - ./app:/deploy/app -------------------------------------------------------------------------------- /docs/image/flask-deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barry-ran/flask-scaffolding/4001614fa9d298a6c54438347ed5eca9c2c78ef8/docs/image/flask-deploy.png -------------------------------------------------------------------------------- /docs/image/run-debug1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barry-ran/flask-scaffolding/4001614fa9d298a6c54438347ed5eca9c2c78ef8/docs/image/run-debug1.png -------------------------------------------------------------------------------- /docs/image/run-debug2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barry-ran/flask-scaffolding/4001614fa9d298a6c54438347ed5eca9c2c78ef8/docs/image/run-debug2.png -------------------------------------------------------------------------------- /docs/image/run-python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barry-ran/flask-scaffolding/4001614fa9d298a6c54438347ed5eca9c2c78ef8/docs/image/run-python.png -------------------------------------------------------------------------------- /docs/image/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barry-ran/flask-scaffolding/4001614fa9d298a6c54438347ed5eca9c2c78ef8/docs/image/screenshot.png -------------------------------------------------------------------------------- /docs/image/select-pipenv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barry-ran/flask-scaffolding/4001614fa9d298a6c54438347ed5eca9c2c78ef8/docs/image/select-pipenv.png -------------------------------------------------------------------------------- /gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | debug = False 3 | bind = "0.0.0.0:5000" 4 | pidfile = "gunicorn.pid" 5 | workers = multiprocessing.cpu_count()*2 + 1 6 | worker_class = "gevent" 7 | # daemon=True 在docker中不需要daemon运行,反而会导致看不到gunicorn输出而增加排查问题的难度 -------------------------------------------------------------------------------- /nginx_flask.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | location / { 6 | proxy_pass http://0.0.0.0:5000; # 这里是指向 gunicorn host 的服务地址 7 | proxy_set_header Host $host; 8 | proxy_set_header X-Real-IP $remote_addr; 9 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 10 | } 11 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.6.5 2 | APScheduler==3.7.0 3 | certifi==2021.5.30 4 | charset-normalizer==2.0.3 5 | click==8.0.1 6 | colorama==0.4.4 7 | Flask==2.0.1 8 | Flask-APScheduler==1.12.2 9 | Flask-Migrate==3.0.1 10 | Flask-SQLAlchemy==2.5.1 11 | greenlet==1.1.0 12 | idna==3.2 13 | importlib-metadata==4.6.1 14 | itsdangerous==2.0.1 15 | Jinja2==3.0.1 16 | Mako==1.1.4 17 | MarkupSafe==2.0.1 18 | PyMySQL==1.0.2 19 | python-dateutil==2.8.2 20 | python-dotenv==0.19.0 21 | python-editor==1.0.4 22 | pytz==2021.1 23 | requests==2.26.0 24 | six==1.16.0 25 | SQLAlchemy==1.4.22 26 | typing-extensions==3.10.0.0 27 | tzlocal==2.1 28 | urllib3==1.26.6 29 | Werkzeug==2.0.1 30 | zipp==3.5.0 31 | -------------------------------------------------------------------------------- /script/compose-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker-compose down 3 | # 不带-d,调试用 4 | # docker-compose up --build 5 | docker-compose up -d --build 6 | -------------------------------------------------------------------------------- /script/docker-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker container stop flask-scaffolding 3 | docker container rm flask-scaffolding 4 | docker image rm flask-scaffolding 5 | 6 | docker build -t flask-scaffolding:latest . 7 | # -v 命令必须用绝对路径,而compose配置可以用相对路径 8 | # 调试用:前台运行 9 | #docker run -p 5000:80 --name=flask-scaffolding -v $(pwd)/app:/deploy/app flask-scaffolding 10 | # 调试用:后台运行并且不退出 11 | #docker run -dit -p 5000:80 --name=flask-scaffolding -v $(pwd)/app:/deploy/app flask-scaffolding 12 | docker run -d -p 5000:80 --name=flask-scaffolding -v $(pwd)/app:/deploy/app flask-scaffolding 13 | -------------------------------------------------------------------------------- /script/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 先尝试迁移数据库 3 | flask db init -d ./app/migrations 4 | flask db migrate -d ./app/migrations 5 | flask db upgrade -d ./app/migrations 6 | # 再启动supervisord 7 | /usr/bin/supervisord 8 | -------------------------------------------------------------------------------- /supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | 4 | [program:nginx] 5 | command=/usr/sbin/nginx 6 | 7 | [program:gunicorn] 8 | command=gunicorn "app:create_app('production')" -c /deploy/gunicorn.conf.py 9 | directory=/deploy --------------------------------------------------------------------------------