简介面向Python Web开发者的一份实用网站模板基于Flask、LayUI与MySQL构建适合需要快速搭建后台管理系统或学习前后端整合的初学者与项目开发者。压缩包共233个文件涵盖58个Python源码、32个HTML页面、36个JavaScript脚本、9个CSS样式以及GIF演示动图、图标字体、配置文件等辅助内容整体仅1.29MB结构轻量且便于按功能模块查阅。目前已有1583人浏览学习可用作个人项目的起步脚手架。模板中内置了前后端交互示例、页面布局与样式、数据库相关代码并配有动图说明运行效果能帮助读者理解Flask路由、模板渲染与LayUI组件的实际结合方式减少从零搭建环境的成本。1. Flask 写后端、LayUI 做界面、MySQL 存数据这套模板帮你跳过最枯燥的那层壳挺多人学 Flask 学到路由、请求都会了卡在最尴尬的一步接口写完了界面怎么又快又像样用原生 HTML 拼页面写十个表单能把手写酸直接上 Vue又得补一整套前端工程化。这套 python flask LayUI mysql 网站模板把三件事打包在一起Flask 负责路由和接口LayUI 负责表格、弹窗、表单这些现成组件MySQL 负责数据落库。模板里带好初始化 SQL、配置文件和能直接跑起来的前后端骨架你改掉数据库密码、跑一遍建表脚本就能在本地把页面点开。适合两种人刚学完 Flask 基础、想快速拿出完整项目的新手以及要在两周内交付内部系统的小团队。2. 动手前先看懂模板目录、建表脚本和配置项先把基础环境过一遍。如果你还在照着 python 安装教程装解释器记得把 Add Python to PATH 勾上不然后面 pip 装依赖会报“不是内部或外部命令”。MySQL 没装的话去官网下载安装包装完 root 密码记牢一会儿 config.py 里要填。下面几步按顺序走可以避掉大多数“模板跑不起来”的求助帖。2.1 目录结构拆解哪个文件是入口哪个目录放页面解压后别急着双击 app.py。先把目录结构过一遍知道哪里是入口、哪里有配置、哪里放页面。一个标准的 Flask LayUI MySQL 模板目录通常是这样的website_template/ ├── app.py # Flask 入口注册路由和蓝图 ├── config.py # 数据库连接、密钥、端口配置 ├── requirements.txt # 依赖清单pip 安装用 ├── db/ │ └── init.sql # 建库建表脚本 ├── static/ │ └── layui/ # LayUI 的 css、js、字体文件 ├── templates/ │ ├── base.html # 公共父模板导航和底部都在这里 │ ├── index.html # 首页 │ ├── user_list.html # 用户列表示例页 │ └── user_form.html # 用户新增/编辑弹窗页 └── utils/ ├── db.py # pymysql 连接封装 └── response.py # 统一 JSON 返回格式app.py 只做路由注册和 Flask 实例初始化业务 SQL 不放这里。config.py 把所有环境相关的东西隔离出来后面部署到服务器只需要改这一个文件。static 目录放 LayUI 的静态资源templates 放 Jinja2 模板utils 下面放数据库连接和响应封装。这个拆法的好处是你后面每加一个模块路径都是固定的templates 多一个页面app.py 多一组路由数据库多两张表互不干扰。templates 和 static 是 Flask 的约定目录。如果你为了好看改了这两个文件夹名Flask 不会自动识别必须创建 Flask 实例时手动指定 template_folder 和 static_folder。模板里用的就是默认约定所以第一次跑之前尽量保持路径别动先跑通再谈重排目录。2.2 MySQL 建表脚本建库语句定死字符集和引擎数据库脚本是整个模板的地基。初始化 SQL 里通常会有建库语句和几张示例表核心代码一般长这样CREATE DATABASE IF NOT EXISTS website_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE website_demo; CREATE TABLE IF NOT EXISTS user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, role TINYINT DEFAULT 1 COMMENT 1 普通用户2 管理员, status TINYINT DEFAULT 0 COMMENT 0 正常1 禁用, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB;第一处要盯着的是 DEFAULT CHARACTER SET utf8mb4不是 utf8。utf8mb4 才是真正的完整 UTF-8能存 Emoji 和生僻字utf8 在 MySQL 里是 utf8mb3 的别名遇到四字节字符会报错或存成问号。第二处是 ENGINEInnoDB别用 MyISAM。如果模板涉及事务和多表关联MyISAM 不支持事务回滚和行级锁删一条数据崩一片的场面就这么来的。字段默认值也值得保留。status TINYINT DEFAULT 0 比允许 NULL 好统计时直接 COUNT 就行不用处理 NULL 转 0。created_at 和 updated_at 建议保留默认写法自动记录创建和更新时间插入的时候少写两个字段。如果你在表结构里看到某张业务表没有这两列最好自己补上几乎所有列表页都要按时间倒序。执行建表脚本命令行方式mysql -u root -p db/init.sql输入密码时屏幕不显示字符这不是卡死。执行完用 mysql -u root -p -e USE website_demo; SHOW TABLES; 确认表建出来了。如果提示权限不足回到 MySQL 给当前用户授权注意别顺手把所有库都放开。开发环境随意生产环境放开就等着被扫描器问候了。2.3 配置文件没改对整个项目起不来模板里最能决定成败的是 config.py。把数据库连接参数改成你本机实际环境我一般这样写import os class Config: SECRET_KEY os.environ.get(SECRET_KEY, dev-secret-key-change-me) DB_HOST os.environ.get(DB_HOST, 127.0.0.1) DB_PORT int(os.environ.get(DB_PORT, 3306)) DB_USER os.environ.get(DB_USER, root) DB_PASSWORD os.environ.get(DB_PASSWORD, 123456) DB_NAME os.environ.get(DB_NAME, website_demo) DB_CHARSET utf8mb4参数含义常见坑DB_HOSTMySQL 地址写 localhost 和 127.0.0.1 在 Linux 上行为不同DB_PORTMySQL 端口不是 3306 时改这里别去改 MySQL 配置DB_USER / DB_PASSWORD登录账号密码密码含特殊字符时注意转义DB_NAME数据库名必须和 init.sql 里的库名一致DB_CHARSET连接字符集和建库字符集不一致会出现乱码代码里用 os.environ.get 提供默认值是给不同环境留口子。本地开发不用设环境变量直接改默认值就能跑部署到 Linux 服务器时用环境变量覆盖避免把数据库密码提交到 Git 仓库。DB_HOST 的坑在第 5 章展开先记着Windows 上 localhost 和 127.0.0.1 差别不大Linux 上 localhost 会让 pymysql 走 Unix socket很多 MySQL 默认不开启那个 socket连不上就报 error 2002。config.py 改完安装依赖。模板里通常有 requirements.txt按下面顺序来cd website_template python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt python app.py用虚拟环境不是为了好看是为了防止服务器上全局 Python 环境里旧版本 Flask 把项目带进尴尬的中间态。pip install 完成后看到 Flask、pymysql 都装上了再启动。如果 python app.py 后提示端口被占用先不要怀疑模板有 bug看一下是不是 5000 端口被别的服务占了。3. Flask 路由与 LayUI 页面渲染从模板继承到接口数据这一章解决“Flask 如何绑定到网页元素”的疑问。LayUI 是纯前端框架Flask 是纯后端二者的连接点是 HTML 模板和 AJAX 接口。写清这一点后面的数据展示、表单提交才有着落。3.1 Jinja2 模板继承把 LayUI 的公共壳抽成一个 base.htmlLayUI 的侧边导航、顶部栏、JS 引入代码在每个页面几乎一样。直接在每页复制粘贴后面改一个菜单名要翻十几个文件。模板里应该用 Jinja2 的继承机制把公共部分收敛到 base.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{% block title %}默认标题{% endblock %}/title link relstylesheet href{{ url_for(static, filenamelayui/css/layui.css) }} {% block head %}{% endblock %} /head body div classlayui-layout layui-layout-admin div classlayui-header顶部导航/div div classlayui-side ul classlayui-nav layui-nav-tree li classlayui-nav-itema href/user/list用户管理/a/li li classlayui-nav-itema href/process/list流程审批/a/li /ul /div div classlayui-body {% block content %}{% endblock %} /div /div script src{{ url_for(static, filenamelayui/layui.js) }}/script {% block script %}{% endblock %} /body /html关键在 url_for。它根据 Flask 的静态目录配置反向拼出资源完整 URL比手写 /static/layui/css/layui.css 更可靠。以后给静态文件加版本号、换 url 前缀手写路径会全部失效url_for 能自动跟随变化。block 是子模板用来覆盖的锚点子页面只需要填自己的内容。子页面继承方式{% extends base.html %} {% block content %} div classlayui-card div classlayui-card-header用户列表/div div classlayui-card-body !-- 这里写自己的内容 -- /div /div {% endblock %}子模板只写 content 部分头部、导航、脚本引用全部从父模板继承。这个结构是模板里最值得保留的设计后面加页面都按这个套路走。如果后面要做权限控制不同角色看不同菜单也是改 base.html 里的菜单块不需要改每个页面。3.2 视图函数与页面数据render_template 传了哪些变量页面要显示数据库里的数据需要一个视图函数准备好数据。模板里典型的列表页路由app.route(/user/list) def user_list_page(): users query_all_users() return render_template(user_list.html, usersusers, page_title用户管理)query_all_users() 在第 4 章实现。这里先理解数据流render_template 的第二个参数开始是模板里能直接使用的变量。user_list.html 里用 Jinja2 循环渲染table classlayui-table thead trthID/thth用户名/thth创建时间/th/tr /thead tbody {% for user in users %} tr td{{ user.id }}/td td{{ user.username }}/td td{{ user.created_at }}/td /tr {% endfor %} /tbody /table这种写法适合数据量小、不需要分页筛选的页面。特点是服务端渲染整页刷新。如果要加搜索筛选再往下走就得上 3.3 的远程表格。有一个坑必须提醒模板里写 user.username要求查询返回的每一行是字典或对象。pymysql 的默认返回结果是元组必须用 DictCursor 才能通过 user[username] 取字段。如果用户列表页一跑就报 jinja2.exceptions.UndefinedError八成是这个没配。返回字段顺序和 SELECT 顺序一旦变化元组方式还可能把用户名和密码位置搞混那时不是报错而是数据错乱更隐蔽。3.3 LayUI 表格吃接口统一 JSON 格式一次配好后端只管返回数据LayUI 的 table 组件支持远程数据自己发 AJAX、渲染表格、处理分页。页面里配置table.render({ elem: #userTable, url: /api/user/list, page: true, cols: [[ { field: id, title: ID }, { field: username, title: 用户名 }, { field: created_at, title: 创建时间 } ]], response: { statusName: code, msgName: msg, countName: count, dataName: data } });对应的 Flask 接口返回结构要跟 response 配置对齐app.route(/api/user/list) def api_user_list(): page int(request.args.get(page, 1)) limit int(request.args.get(limit, 10)) total, rows query_users_page(page, limit) return jsonify({code: 0, msg: , count: total, data: rows})LayUI 的默认约定是 code0 表示成功msg 是错误信息count 是总数data 是当前页数据。后端只要返回这个结构表格自动渲染数据和分页条。字段名对不上就出问题LayUI 期望字段含义后端返回不一致时的表现code0成功标志表格一直转圈不渲染数据msg提示信息失败时不显示原因count数据总数分页总数显示 0data当前页数据表格空白无报错前端自动带上 page 和 limit 参数page 是当前页码limit 是每页条数。后端从 request.args 里读这两个值再传给 SQL 的 LIMIT 和 OFFSET这套配合的关键细节放在第 4.2 节。4. 数据库交互实操连接封装、分页查询、事务与 SQL 安全这一章对应“web 页面详细步骤实现代码和 mysql 数据设计表”的诉求。前端页面只是壳模板真正值钱的是后端和 MySQL 的交互方式以及那几个最容易写错的数据处理细节。4.1 pymysql 连接封装为什么模板常用每次请求新建连接模板里最标准的连接封装是独立的 utils/db.pyimport pymysql from config import Config def get_conn(): return pymysql.connect( hostConfig.DB_HOST, portConfig.DB_PORT, userConfig.DB_USER, passwordConfig.DB_PASSWORD, databaseConfig.DB_NAME, charsetConfig.DB_CHARSET, connect_timeout5, cursorclasspymysql.cursors.DictCursor )两个参数值得解释。connect_timeout5 让 MySQL 没响应时 5 秒就报错不设的话可能等几十秒页面一直转圈。cursorclasspymysql.cursors.DictCursor 让查询结果变成字典列表每行是 {id: 1, username: admin}Jinja2 里写 user.username 才成立。如果你自己写连接漏掉 DictCursor所有模板取值都会报错。这种写法是每次请求新建连接、用完关闭不引入连接池。有人觉得浪费但对中小型内部系统来说代价完全可接受。模板的目标是让你快速跑通不为并发优化。等线上并发上去了再替换成 DBUtils 的 PooledDB 或 SQLAlchemy 连接池接口层不用改只改 get_conn 内部实现。MySQL 的连接成本在局域网内是毫秒级真没到需要提前优化的程度。4.2 分页查询page、limit 怎么变成 SQL 的 LIMIT 和 OFFSET配合 LayUI 表格的分页查询模板里一般是这么写的def query_users_page(page, limit): offset (page - 1) * limit conn get_conn() try: with conn.cursor() as cur: cur.execute(SELECT COUNT(*) AS total FROM user) total cur.fetchone()[total] sql SELECT id, username, role, created_at FROM user ORDER BY id DESC LIMIT %s OFFSET %s cur.execute(sql, (limit, offset)) rows cur.fetchall() return total, rows finally: conn.close()容易算错的是 OFFSET它表示跳过多少条等于 (page - 1) * limit。第 2 页、每页 10 条offset 是 10从第 11 条开始取。很多新手直接写 OFFSET page结果第二页和第一页数据重叠。还有效率问题LIMIT 100000, 10 会让 MySQL 先读十万条再丢弃表大了翻页越来越慢。比较快的做法是记住上一页最后一条 id用 WHERE id 上页边界 ORDER BY id DESC LIMIT 10复杂系统分页优化基本都是这个思路。代码里的 %s 是参数化查询。不要自己拼 SQL比如 fSELECT ... WHERE id {user_id}。用户输入一旦带上 SQL 特殊字符表都可能被删。pymysql 的 execute 传参数会自动转义和类型处理所有涉及用户输入的 SQL 都该这么写。4.3 事务写入两张表要么全成功要么全回滚翻车最多的场景往主表插一条数据、往日志表插一条记录第一条成功第二条报错数据停在“半成”状态。模板里涉及多表写入应该包事务def create_user_with_log(username, password_hash): conn get_conn() try: with conn.cursor() as cur: cur.execute( INSERT INTO user (username, password_hash) VALUES (%s, %s), (username, password_hash) ) new_id cur.lastrowid cur.execute( INSERT INTO user_log (target_id, action) VALUES (%s, %s), (new_id, create_user) ) conn.commit() return new_id except Exception: conn.rollback() raise finally: conn.close()事务边界是 conn.commit() 和 conn.rollback()。commit 之前所有操作都是临时的只有 commit 成功才算落库。中间任何一步异常rollback 会撤销前面的改动。注意 pymysql 默认不自动提交所以你不写 conn.commit()数据看起来进去了数据库一查却是空的。注意except 里 rollback 之后要 raise 继续抛异常让 Flask 接口层捕获并返回给前端。如果吞掉异常调用方以为“新增成功”数据库里什么都没写这种错最难查。4.4 模板里最常见的 SQL 坏味道字符串拼接、裸 SELECT * 和存储过程拿到模板后全局搜一下有没有 execute(f...) 这种写法。有就趁早改成 4.2 的参数化写法不要嫌麻烦。还有 SELECT * 的问题表字段少时看不出毛病等业务加字段接口返回数据变大、前端字段错位排查成本就上去了。把查询字段明确列出来既是给阅读者看接口返回什么也是给前后端留一份数据契约。再说一个是非题存储过程。模板的 SQL 脚本里如果出现存储过程建议别沿用。业务逻辑写在 Python 层里用 Git 能追踪变更存储过程改起来要单独管理脚本团队一多就乱套。同理模板里假如出现 UPDATE 语句不带 WHERE 或者 WHERE 11这属于事故隐患。执行 UPDATE 之前先跑一条等值 SELECT 看影响行数再决定下手。MySQL 的 UPDATE 语法本身不复杂复杂的是你不清楚自己要改多少行。5. 部署避坑与排查模板跑通只是开始过完这四道坎才算真能用下面这几条是拆这类 Flask LayUI MySQL 模板时遇到的真实踩坑记录每条按“现象 → 原因 → 解决”展开。5.1 静态文件 404Flask 静态目录、LayUI 资源与附件路径现象首页能打开但页面没有样式。浏览器 F12 网络面板里 layui.css、layui.js 全部 404用户上传的图片附件也 404点击图片地址直接跳到错误页。原因模板里用 url_for(static, filenamelayui/css/layui.css)但 Flask 实例创建时没指定 static_folder或者静态文件实际放在了别的目录下。还有一种情况是蓝图里设置了 static_folder导致 /static 默认路由没注册。附件路径 404 经常是 Windows 上用 \ 写相对路径部署到 Linux 后目录分隔符对不上或者上传目录在 Flask 静态目录之外。解决先确认 static 文件夹和 app.py 在同一层。不在同一层就在创建 Flask 实例时显式指定 app Flask(name, static_folder../static)。蓝图场景不要在蓝图里配 static_folder统一在应用级别配置。附件路径用 os.path.join(app.config[UPLOAD_FOLDER], filename) 拼并单独加一个映射路由app.route(/uploads/path:filename) def uploaded_file(filename): return send_from_directory(app.config[UPLOAD_FOLDER], filename)send_from_directory 会做一次目录穿越校验比直接拼路径安全。改完重启进程访问一次资源路径确认返回 200。模板自带 LayUI 资源时这种 404 九成是目录层级搬动导致别直接改 HTML 里的路径去碰运气。5.2 MySQL 连接报错ERROR 2002 无法通过 socket 连上现象在 Linux 服务器上跑 app.py启动时报 pymysql.err.OperationalError: (2002, Cant connect to local MySQL server through socket /tmp/mysql.sock)。原因config.py 里 DB_HOST 写的是 localhostpymysql 在 Linux 上会默认走 Unix socket 文件不是 TCP 连接。服务器上的 MySQL 没把 socket 文件放在默认位置或者没开启 socket就连接不上。解决把 DB_HOST 改成 127.0.0.1强制走 TCP 连 3306 端口绕开 socket 路径问题。这个改法在 Windows、Linux、macOS 上通用。改完先用命令行验证通道mysql -h 127.0.0.1 -P 3306 -u root -p如果能登上说明 TCP 通道没问题Flask 那边也能通。如果报错再检查 MySQL 的 bind-address 是否绑定了 127.0.0.1以及用户授权里有没有 root127.0.0.1 这个条目。MySQL 用户表里 rootlocalhost 和 root127.0.0.1 是两条独立授权只授权 localhost 时 TCP 连接会被拒绝。5.3 中文乱码数据库、连接、页面三处字符集对齐现象LayUI 页面上显示用户名是问号或“鍚堟硶”这种乱码数据库里查询也是乱码。还有一种是把 init.sql 里的中文注释变成乱码建进表里。原因三层字符集没对齐。第一层是建库时指定字符集第二层是 pymysql 连接时传的 charset第三层是 HTML meta 里的 charset。任何一层不一致就乱码。如果是在 Windows cmd 里用 mysql init.sql 导入cmd 默认编码是 GBK脚本里的中文注释也会被当成 GBK 写入脏数据直接进库。解决把三层统一成 utf8mb4。init.sql 建库语句用 utf8mb4get_conn 里 charset 写 utf8mb4base.html 的 meta charset 改成 UTF-8。已建好的库执行 ALTER DATABASE website_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。导入脚本时加上编码参数mysql --default-character-setutf8mb4 -u root -p db/init.sql改完重新插一条中文数据页面和数据库两边都看一眼。连接层和存储层最容易漏页面层一般默认没问题。5.4 端口占用、依赖冲突与路径怪癖本地跑通、线上挂掉的典型原因现象本地 python app.py 一切正常部署到服务器后提示端口被占用或 ImportError 找不到某个模块或上传文件后不知道文件存到了哪。原因第一种是 5000 端口被服务器其他服务占了。第二种是服务器全局 Python 环境里缺依赖或有旧版 Flask 冲突。第三种是代码里写死了相对路径“uploads/”当前工作目录一变文件就写到别处去了。解决端口冲突改 Flask 启动端口代码里写成可配的if __name__ __main__: port int(os.environ.get(PORT, 5001)) app.run(host0.0.0.0, portport)依赖问题在项目目录建虚拟环境按顺序执行 python -m venv venv、source venv/bin/activate、pip install -r requirements.txt。项目上线时用 pip freeze requirements.txt 把当前环境版本固下来别人 clone 下来不会因为依赖版本不同翻车。路径问题用 os.path.join(app.root_path, uploads)root_path 是 Flask 根据生成目录算好的绝对路径不随启动目录变化。这三类问题在部署阶段几乎必现提前处理能省一整晚。6. 改造这门手艺给模板加一个模块、一层权限、一道搜索模板的真正价值在于二次开发。这里给一条被验证过的路径照做不会把项目改坏。6.1 加一个业务模块的最短路径拿“用 LayUI 设计流程审批”来说模板没有审批功能但你可以按固定路径加上去。第一步在 init.sql 加一张 process 表字段至少包括 id、title、creator、status、created_at。第二步在 app.py 注册一个蓝图 process_bp里面写两个路由一个是页面渲染路由一个是数据接口路由。第三步在 templates 下新建 process_list.html继承 base.html再用 table.render 指到接口。第四步在 base.html 侧边菜单加一个“流程审批”入口。四步做完一个能分页的审批列表就出来了后续的步骤条显示、审批按钮都是在这个骨架上加。6.2 给页面加登录会话拦截器到底拦什么内部系统十有八九要登录。模板如果没有 session 控制加一个 before_request 钩子最快app.before_request def check_login(): allow_list [/login, /static] if request.path.startswith(tuple(allow_list)): return None if not session.get(user_id): return redirect(url_for(login_page))这个钩子对每个请求都会执行。allow_list 放不校验的路径比如登录页和静态资源。其他路径如果 session 里没有 user_id直接重定向到登录页。注意 /static 必须放行否则 LayUI 的 css、js 会被拦截登录页直接裸奔。6.3 搜索与关键词匹配从 LIKE 到简单相似度模板自带的列表页多数是查全部业务上加搜索最常见的就是 SQL 模糊查询SELECT * FROM process WHERE title LIKE %s OR creator LIKE %s参数是 %关键词%注意用参数化方式传入不能拼字符串。如果你在做农产品价格数据可视化这种按名称筛选的系统这个够用。如果要做校园失物招领那种关键词相似度匹配LIKE 只能做全词匹配做不到“手机”“手机壳”这种相关推荐。低成本方案把标题和描述拆成关键词列表用 Python 的集合交集算重合度重叠超过阈值视为相关。算法不复杂效果比 LIKE 高一个档次。顺着这个思路你就能把模板升级成带智能匹配的小平台比到处找现成系统更靠谱。最后说一个我自己的习惯。每次拿到这类模板我不急着改功能先把 2.3 的配置改好、把 init.sql 跑一遍、用 3.3 的接口测通一个表格确认前后端链路是通的才开始动业务代码。这个“先跑通再改造”的顺序让我少踩了很多“改了半天不知道是前端还是后端问题”的坑。从那以后我每次接手 Flask 项目都强制走一遍这个流程先让骨架活起来再谈增删改查。这套模板够轻、够直观按这个顺序走下来大概率一晚上就能看到自己的第一个页面。希望帮到你。本文还有配套的精品资源点击获取