简介:这是一套基于 Python 技术栈的网站开发模板,采用 Flask 作为后端框架,前端整合 LayUI 与 Bootstrap 等 UI 组件,数据层使用 MySQL 存储,适合正在学习 Web 全栈开发、需要快速搭建后台管理或企业官网的初中级开发者参考使用。压缩包共 233 个文件,约 1.29MB,其中 58 个 py 文件承载路由与业务逻辑,32 个 html 页面与 36 个 js、9 个 css 文件构成前端界面与交互,另含 gif、png、jpg 等图片素材及字体、图标资源,目录结构完整,可直接运行或按需二次开发。目前已有 1584 人学习下载。模板覆盖登录、表格、弹层、日期选择等常见页面模块,读者可借此理解 Flask 项目分层组织方式、前后端数据交互流程与静态资源引用规范,并在此基础上替换业务逻辑,快速完成课程设计、毕业设计或个人练手项目。
1. 拿到一个「python + flask + LayUI + mysql 网站模板.zip」之后,先别急着解压
很多人拿到这类压缩包的第一反应是双击解压、找app.py、pip install、python app.py,然后浏览器一开发现 500,回头骂模板垃圾。我见过太多这种翻车现场。这个标题其实描述的是一个非常具体的组合:后端用 Flask 做路由和业务逻辑,前端用 LayUI 做后台管理界面,数据落在 MySQL 里,整体打包成一个可以直接改的网站骨架。它解决的不是「从零写一个网站」的问题,而是「我已经知道要做什么功能,但不想再花三天搭登录页和表格页」的问题。适合谁?适合需要快速交付中小型管理系统、后台工具、内部平台的开发者,尤其是那些前端不想碰 Vue/React 全家桶、只想用 jQuery 风格写页面的人。这一章不写代码,先把这套组合的边界和预期讲清楚,后面五章再动手。
2. 把模板跑起来之前,先搞清楚 Flask + LayUI + MySQL 各自负责什么
2.1 三层各管一段,别让它们互相越界
Flask 在这个组合里只做三件事:接收 HTTP 请求、调用业务函数、返回 JSON 或渲染模板。它不负责生成 HTML 结构,也不负责数据库表设计。LayUI 负责浏览器里看到的一切:表格、表单、弹层、分页、日期选择器。它通过 AJAX 向 Flask 要数据,拿到 JSON 后自己渲染。MySQL 只负责存数据,表结构、索引、字符集这些事在 Flask 之外就要定好。
常见的新手错误是让 Flask 直接拼 HTML 表格,然后 LayUI 的 table 模块完全没用上。这样写出来的页面又丑又难维护。正确的分工是:Flask 返回{"code": 0, "msg": "", "count": 100, "data": [...]}这种格式,LayUI 的table.render直接消费。
另一个边界问题是数据库连接。Flask 里不要每次请求都pymysql.connect(),也不要用全局单连接。常见做法是用DBUtils的PooledDB做连接池,或者用Flask-SQLAlchemy的scoped_session。我一般会选后者,因为模板里通常已经带了models.py,改起来顺手。
2.2 最小可运行环境的搭建步骤
假设你已经解压到一个目录,里面能看到app.py、requirements.txt、static/、templates/、sql/这几个东西。先别改代码,按下面顺序走。
第一步,建虚拟环境并装依赖。不要用系统 Python 直接装,版本冲突会让你怀疑人生。
python -m venv venv # Windows 用 venv\Scripts\activate source venv/bin/activate pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple第二步,建库并导入初始 SQL。模板里的sql/init.sql通常包含建表语句和一条管理员账号。
mysql -u root -p -e "CREATE DATABASE demo_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;" mysql -u root -p demo_db < sql/init.sql第三步,改配置文件。Flask 模板一般把数据库连接写在config.py或app.config里,找到SQLALCHEMY_DATABASE_URI或pymysql.connect的参数,改成你自己的。
# config.py 片段 import os class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-key-change-me' MYSQL_HOST = '127.0.0.1' MYSQL_PORT = 3306 MYSQL_USER = 'root' MYSQL_PASSWORD = 'your_password' MYSQL_DB = 'demo_db' SQLALCHEMY_DATABASE_URI = ( f"mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}" f"@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DB}?charset=utf8mb4" ) SQLALCHEMY_TRACK_MODIFICATIONS = False第四步,启动。python app.py或flask run,看控制台有没有报错。如果看到ModuleNotFoundError,说明依赖没装全;如果看到Access denied,说明数据库账号密码不对;如果看到Unknown database,说明库没建。
提示:模板里的
SECRET_KEY一定要改,不要用默认值上线。这个 key 泄露意味着 session 可以被伪造。
2.3 目录结构里哪些文件必须改,哪些可以不动
一个典型的 Flask + LayUI 模板目录长这样:
project/ ├── app.py ├── config.py ├── models.py ├── requirements.txt ├── static/ │ ├── layui/ │ ├── css/ │ └── js/ ├── templates/ │ ├── login.html │ ├── index.html │ └── user/ │ └── list.html └── sql/ └── init.sql必须改的:config.py里的数据库和密钥,sql/init.sql里的默认账号密码,templates/login.html里的标题和 logo。可以不动但建议看的:models.py里的表定义,app.py里的路由蓝图注册方式。不要动的:static/layui/整个目录,除非你要升级 LayUI 版本。
我见过有人把static/layui/里的文件删了几个,说「用不到」,结果表格分页直接消失。LayUI 的模块是按需加载的,但layui.js和layui.css必须完整。
3. 用 Flask 写第一个业务接口:从 LayUI 表格请求到 JSON 返回
3.1 LayUI 表格的数据契约长什么样
LayUI 的table模块默认会向url发一个 GET 请求,带上page和limit两个参数。它期望的响应格式是固定的:
{ "code": 0, "msg": "", "count": 123, "data": [ {"id": 1, "username": "a", "status": 1} ] }code为 0 表示成功,非 0 会触发错误提示。count是总记录数,用于分页计算。data是当前页的数据数组。这个契约不能改,改了 LayUI 就不认。
所以 Flask 这边的接口必须做两件事:接收page和limit,查数据库时用LIMIT offset, limit,同时查一次总数。
3.2 一个可复用的分页查询函数
不要每个接口都写一遍分页逻辑。我一般会在utils.py里放一个通用函数:
# utils.py from models import db from sqlalchemy import text def paginate_query(base_sql, count_sql, params, page, limit): """ base_sql: 带 LIMIT 占位的数据查询语句 count_sql: 统计总数的语句 params: 参数字典 page: 当前页,从 1 开始 limit: 每页条数 返回 (data_list, total_count) """ offset = (page - 1) * limit # 先查总数 total = db.session.execute(text(count_sql), params).scalar() # 再查当前页 page_params = dict(params) page_params['offset'] = offset page_params['limit'] = limit rows = db.session.execute(text(base_sql), page_params).fetchall() # 把 Row 对象转成 dict data = [dict(row._mapping) for row in rows] return data, total逻辑说明:count_sql和base_sql分开传,是因为有些查询带JOIN时统计总数不能直接套。offset和limit用参数绑定,不要用字符串拼接,否则 SQL 注入风险直接拉满。row._mapping是 SQLAlchemy 1.4+ 的写法,老版本用dict(row)。
参数说明:page和limit从request.args.get('page', 1, type=int)拿,limit默认给 10 或 20。注意 LayUI 传过来的limit可能是字符串,用type=int转一下。
3.3 路由里怎么接住 LayUI 的请求
# app.py 片段 from flask import request, jsonify from utils import paginate_query @app.route('/api/user/list') def user_list(): page = request.args.get('page', 1, type=int) limit = request.args.get('limit', 10, type=int) keyword = request.args.get('keyword', '', type=str) where = "WHERE 1=1" params = {} if keyword: where += " AND username LIKE :kw" params['kw'] = f"%{keyword}%" base_sql = f""" SELECT id, username, status, created_at FROM sys_user {where} ORDER BY id DESC LIMIT :offset, :limit """ count_sql = f"SELECT COUNT(*) FROM sys_user {where}" data, total = paginate_query(base_sql, count_sql, params, page, limit) return jsonify({ "code": 0, "msg": "", "count": total, "data": data })逻辑说明:where子句动态拼,但值全部走参数绑定。ORDER BY id DESC保证新数据在前。返回的data里created_at如果是datetime对象,jsonify会自动转成字符串,但格式可能不是你想要的,必要时在 SQL 里用DATE_FORMAT处理。
参数说明:keyword是可选搜索条件,LayUI 的表格搜索栏会把它作为额外参数传过来。limit不要设太大,超过 100 会让前端渲染变慢。
注意:如果模板用的是
Flask-SQLAlchemy的 ORM 查询,分页可以用query.paginate(page, per_page, error_out=False),但返回结构要自己转成 LayUI 格式。两种方式都行,看模板原本怎么写的。
4. LayUI 前端表格和表单的对接细节,以及 MySQL 表设计的三个硬约束
4.1 table.render 的参数怎么和 Flask 接口对齐
LayUI 的表格初始化代码通常写在templates/user/list.html里:
// static/js/user_list.js layui.use(['table', 'form'], function () { var table = layui.table; var form = layui.form; table.render({ elem: '#userTable', url: '/api/user/list', method: 'get', page: true, limit: 10, limits: [10, 20, 50], cols: [[ {field: 'id', title: 'ID', width: 80, sort: true}, {field: 'username', title: '用户名', minWidth: 120}, {field: 'status', title: '状态', width: 100, templet: function (d) { return d.status === 1 ? '启用' : '禁用'; }}, {field: 'created_at', title: '创建时间', width: 180}, {title: '操作', toolbar: '#rowToolbar', width: 150} ]], parseData: function (res) { // 如果后端返回的字段名和 LayUI 默认不一致,在这里转换 return { code: res.code, msg: res.msg, count: res.count, data: res.data }; } }); // 搜索按钮 form.on('submit(searchBtn)', function (data) { table.reload('userTable', { where: data.field, page: {curr: 1} }); return false; }); });逻辑说明:parseData是可选的,如果后端已经返回code/msg/count/data,可以省略。templet用来做字段的显示转换,比如状态码转文字。table.reload的where会把搜索表单的字段作为额外参数拼到请求里,Flask 那边用request.args.get接。
参数说明:limits控制每页条数下拉选项,不要设太多。page: {curr: 1}表示搜索后回到第一页,不加的话会停留在当前页导致看不到结果。
4.2 MySQL 表设计的三个硬约束
第一,字符集必须utf8mb4,排序规则utf8mb4_unicode_ci。不要用utf8,那个是残废的,存不了 emoji 和部分生僻字。建库建表都要显式指定。
第二,主键用BIGINT UNSIGNED AUTO_INCREMENT,不要用INT。现在数据量涨得快,INT上限 21 亿,看着多,但日志表、操作记录表很容易超。BIGINT一步到位。
第三,时间字段用DATETIME而不是TIMESTAMP。TIMESTAMP有 2038 年问题,而且受时区影响。DATETIME存什么就是什么,配合DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP足够用。
CREATE TABLE `sys_user` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `username` VARCHAR(64) NOT NULL COMMENT '登录名', `password_hash` VARCHAR(255) NOT NULL COMMENT '密码哈希', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0禁用', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统用户表';逻辑说明:password_hash存哈希不存明文,用werkzeug.security.generate_password_hash生成。uk_username唯一索引防止重复注册。status用TINYINT而不是BOOL,因为 MySQL 的BOOL本质就是TINYINT(1),不如直接写清楚。
参数说明:VARCHAR(64)对用户名够用,VARCHAR(255)对哈希够用。不要用TEXT存这些短字段,索引效率会差。
4.3 登录态怎么在 Flask 和 LayUI 之间传递
模板通常用 Flask 的session存登录状态。登录成功后session['user_id'] = user.id,然后每个需要登录的接口用装饰器检查。
from functools import wraps from flask import session, jsonify, redirect, url_for def login_required(f): @wraps(f) def decorated(*args, **kwargs): if 'user_id' not in session: # 如果是 AJAX 请求,返回 JSON;否则跳转登录页 if request.is_json or request.path.startswith('/api/'): return jsonify({"code": 401, "msg": "未登录"}), 401 return redirect(url_for('login')) return f(*args, **kwargs) return decorated逻辑说明:LayUI 的 AJAX 请求如果收到 401,可以在全局ajaxSetup里统一跳转登录页。不要在每个接口里单独处理。
参数说明:request.is_json判断请求体是不是 JSON,request.path.startswith('/api/')是兜底判断。两个条件满足一个就走 JSON 返回。
5. 避坑:这套组合里最容易翻车的五个地方
5.1 中文乱码:从数据库到浏览器全链路排查
现象:页面上的中文显示成???或测试。原因:字符集在某一层断了。解决:按顺序查。第一,MySQL 库和表的字符集是不是utf8mb4,用SHOW CREATE TABLE sys_user;看。第二,Flask 连接串里有没有?charset=utf8mb4。第三,HTML 的<meta charset="utf-8">有没有。第四,Python 文件本身是不是 UTF-8 编码。四个地方都对了,乱码就没了。
5.2 LayUI 表格不显示数据,但接口返回正常
现象:浏览器 Network 里看到接口返回了 JSON,但表格是空的。原因:parseData没写对,或者返回的code不是 0。解决:先看返回的code值,LayUI 只认 0。如果后端返回的是{"code": 200},要么改后端,要么在parseData里把 200 转成 0。另外检查data字段是不是数组,如果返回的是{"data": {"list": [...]}},需要parseData里取res.data.list。
5.3 分页总数不对,翻到第二页数据重复
现象:第一页和第二页数据一样,或者总数显示 0。原因:count_sql写错了,或者LIMIT的offset计算错了。解决:offset = (page - 1) * limit,这个公式不能错。count_sql不要带LIMIT,也不要用SELECT *包一层再COUNT,直接SELECT COUNT(*) FROM 表 WHERE 条件。如果带JOIN,注意COUNT(DISTINCT 主表.id)。
5.4 修改了 static 里的文件,浏览器不生效
现象:改了 CSS 或 JS,刷新页面没变化。原因:浏览器缓存。解决:开发阶段按Ctrl+F5强制刷新,或者在 Flask 里配置SEND_FILE_MAX_AGE_DEFAULT = 0。上线后要给静态文件加版本号,比如layui.css?v=1.0.1,否则用户缓存旧文件会导致页面错乱。
5.5 数据库连接数暴涨,MySQL 报 too many connections
现象:运行一段时间后接口全部 500,MySQL 日志显示连接数满。原因:每次请求都新建连接且没关闭,或者连接池配置过大。解决:用连接池,SQLALCHEMY_ENGINE_OPTIONS里设pool_size=5, max_overflow=10, pool_recycle=3600。pool_recycle很重要,MySQL 默认 8 小时断开空闲连接,不设这个会拿到失效连接。
# config.py 补充 SQLALCHEMY_ENGINE_OPTIONS = { 'pool_size': 5, 'max_overflow': 10, 'pool_recycle': 3600, 'pool_pre_ping': True }逻辑说明:pool_pre_ping会在每次取连接时先 ping 一下,失效的连接自动重建。pool_recycle=3600表示连接存活超过 1 小时就回收,避免被 MySQL 服务端断开。
6. 把这套模板改造成可交付项目的三个进阶习惯
6.1 用蓝图拆分路由,别让 app.py 变成千行怪
模板初始的app.py通常把所有路由都堆在一起。功能一多,这个文件就没法看了。我一般会在项目中期做一次拆分:按业务模块建views/目录,每个模块一个蓝图文件。
# views/user.py from flask import Blueprint, request, jsonify, session from utils import paginate_query, login_required from models import db user_bp = Blueprint('user', __name__, url_prefix='/api/user') @user_bp.route('/list') @login_required def list_users(): # 具体逻辑 pass @user_bp.route('/add', methods=['POST']) @login_required def add_user(): # 具体逻辑 pass然后在app.py里注册:
from views.user import user_bp app.register_blueprint(user_bp)逻辑说明:url_prefix统一加前缀,蓝图内部路由写相对路径。这样app.py只负责创建应用、加载配置、注册蓝图,保持在 50 行以内。
参数说明:蓝图名不要和模块名冲突,user_bp比user安全。url_prefix结尾不要带斜杠,否则路由会变成//list。
6.2 给 LayUI 表格加一个统一的请求拦截器
LayUI 底层用的是 jQuery 的$.ajax,可以在页面加载时统一设置ajaxSetup,处理 401 跳转和全局错误提示。
// static/js/common.js layui.use(['layer', 'jquery'], function () { var $ = layui.jquery; var layer = layui.layer; $.ajaxSetup({ complete: function (xhr) { if (xhr.status === 401) { layer.msg('登录已过期,请重新登录', {icon: 2}); setTimeout(function () { top.location.href = '/login'; }, 1500); } } }); });逻辑说明:complete回调在请求结束后触发,不管成功还是失败。判断xhr.status === 401就跳转。用top.location.href而不是location.href,防止页面被嵌套在 iframe 里跳不出去。
参数说明:layer.msg的icon: 2是错误图标。setTimeout给用户 1.5 秒看提示,不要立即跳。
6.3 上线前必做的三项检查
第一,关掉DEBUG。app.run(debug=True)只能开发用,上线必须debug=False,否则报错页面会暴露源码路径和配置信息。
第二,改掉所有默认密码。包括数据库密码、SECRET_KEY、初始管理员密码。模板里的默认值都是公开的,不改等于没锁门。
第三,配一个生产级 WSGI 服务器。Flask 自带的开发服务器扛不住并发,用gunicorn或uwsgi。gunicorn -w 4 -b 127.0.0.1:8000 app:app是最简配置,-w 4表示 4 个 worker 进程,一般设成 CPU 核数的 2 倍。
# 生产启动示例 gunicorn -w 4 -b 127.0.0.1:8000 --access-logfile - --error-logfile - app:app逻辑说明:--access-logfile -把访问日志打到标准输出,方便被容器日志收集。app:app表示app.py文件里的app对象。
参数说明:-w不要设太大,超过 CPU 核数 2 倍反而会因为上下文切换降低性能。如果用了gevent或eventlet,可以用-k gevent切协程模式。
我自己的习惯是,每次拿到一个新模板,先花 20 分钟把上面这些检查过一遍,再开始写业务代码。这个习惯帮我省掉了至少三次上线当天的紧急回滚。希望帮到你。
本文还有配套的精品资源,点击获取