
【Python毕设源码分享】基于Flask Vue 的博客系统设计与实现(程序文档代码讲解一条龙定制)又到了一年一度赶毕设的高峰期。我身边好几个同学都来问我做个博客系统行不行Python后端配什么前端框架好 问得多了干脆把我之前梳理过的基于Flask Vue的博客系统完整思路拿出来聊聊。如果你正愁毕设题目或者想练手全栈开发这篇文章基本能帮你把一个博客系统到底怎么做整个链路捋清楚从技术选型、环境搭建、数据库设计到前后端打通、部署上线一次讲明白。先说结论用 Flask 写后端接口用 Vue 写前端页面是目前做毕设最稳的组合之一。不是因为它们是最新的技术而是因为它们足够的标准。Flask 轻量、灵活、文档多Vue 有完整的中文生态和脚手架工具两者搭配起来既能体现你对后端接口设计的理解也能展示出你对现代前端工程化的掌握。说白了毕业设计答辩的时候老师问什么你都能接得住。这个博客系统能做什么简单来说就是前端展示文章列表、文章详情、评论互动后端管理文章、分类、标签、评论再加上用户登录注册和权限控制。麻雀虽小五脏俱全。一个合格的博客系统应该把这些功能点全部覆盖到。文章里不仅有完整的源码思路还会告诉你每一步为什么要这么做以及哪些地方容易踩坑。如果你是非科班转行、基础一般或者时间比较紧这套方案尤其适合。它的代码量适中逻辑清晰能很好地展示你的工程能力。我会把整个设计和实现过程拆解成可操作的步骤项目该有的东西——程序、文档、讲解、定制——我都会聊到方便你按需取用。1. 为什么是 Flask Vue技术选型背后的现实逻辑很多人在选技术栈的时候容易陷入一个误区谁火选谁。今天听说 Go 很火明天看到 Next.js 很酷结果学了一堆东西最后发现根本用不上或者用起来处处受阻。毕设项目最重要的是什么是能自己讲清楚、能完整跑起来、能通过答辩。1.1 后端选 Flask 的真实理由Flask 是一个非常经典的 Python Web 微框架。它微在哪里微在核心而不是功能。官方自带的东西很少但它的扩展机制非常丰富你完全可以根据项目需要像拼积木一样把各种功能模块加进去。拿我们这个博客系统来说常用的扩展有这么几个Flask-SQLAlchemy数据库 ORM用来操作 MySQL 或 SQLite不必自己写原生 SQL。Flask-Migrate数据库迁移工具模型改了之后自动生成迁移脚本不用手动删表重建。Flask-JWT-ExtendedJWT 登录鉴权前后端分离场景下最常见的认证方案。Flask-Cors处理跨域请求前后端分离部署时必备。Marshmallow序列化与反序列化把 ORM 模型转换成 JSON 返回给前端。这样的好处是框架本身的代码很薄你很清楚每一个模块是干什么的答辩的时候老师问你用到了哪些库你可以条理清晰地列出来还能讲清楚每个库解决什么问题。这比用一个全家桶式框架在里面稀里糊涂写代码要加分得多。1.2 前端选 Vue 的过人之处Vue 的优势也体现在学习曲线平缓上。它不像 React 那样有较多的函数式编程概念模板写法更贴近传统的 HTML 结构。对于一个以 Python 为主力语言、前端经验没那么深的同学来说Vue 的上手成本确实是最低的。而且 Vue 3 的 Composition API 写起来非常舒服逻辑组织更清晰配合 Vite 这个构建工具启动一个开发服务器只要一两秒热更新也是即改即见。我们的博客系统前端部分需要的东西Vue Router 管路由、Pinia 管全局状态、Axios 发请求全部都是标准方案遇到问题一搜就有答案。1.3 前后端分离毕设项目最值得展示的亮点传统的 PHP 项目是前后端不分离的页面和逻辑混在一起。而Flask 后端 Vue 前端是标准的前后端分离架构。这个架构本身在简历上、答辩 PPT 上就是能拿出来讲的亮点后端只提供 API 接口前端负责渲染和交互数据通过 JSON 格式交互。这样做的好处有三个。第一职责清晰前端只管界面后端只管业务逻辑和数据第二开发并行定好接口文档之后前后端可以同时开工互不干扰第三部署灵活前端构建后是一堆静态文件可以扔到 Nginx 里后端跑在 Flask 自带的服务器上甚至未来想迁移云函数、容器化部署都很方便。2. 博客系统的功能设计与数据库建模选好了技术栈下一步就是设计功能。做之前先画一张大图心里有数再动手。博客系统典型的功能模块可以划分为前台展示和后台管理两大块。2.1 前台与后台功能清单前台展示给普通访客看应包含以下功能文章列表页分页展示文章显示标题、摘要、封面图、发布时间、分类、阅读量。文章详情页渲染文章正文显示作者信息、发布时间、分类、标签。评论区登录用户可以发表评论支持嵌套回复。文章搜索按标题或内容关键词模糊搜索。分类与标签页按分类、标签筛选文章。个人主页展示博主信息和文章归档。后台管理仅供登录管理员使用仪表盘文章总数、评论总数、分类总数、用户总数等数据概览。文章管理实现增删改查、发布/下架、置顶操作。分类管理新增、重命名、删除分类。标签管理新增、合并、删除标签。评论管理查看评论列表、删除违规评论。用户管理查看注册用户列表、分配或取消管理员权限。你可能觉得功能有点多但真实做下来就会发现这些功能之间是天然关联的。做了文章管理就必然要做分类和标签有评论就必须考虑评论管理和权限控制。把这些做好一个博客系统的完整性、可用性就都体现出来了。2.2 数据库设计五张核心表就够了我用的是 MySQL原因无他毕设环境里 MySQL 是最常见的数据库老师也认可。你也可以换成 SQLite零配置直接跑更适合演示环境。但为了规范我建议优先使用 MySQL。数据库里最核心的表有 5 张users用户表存用户 ID、用户名、加密后的密码、邮箱、头像、角色普通用户/管理员、注册时间。articles文章表存文章 ID、标题、摘要、正文Markdown 格式、封面图 URL、阅读量、状态草稿/已发布、创建时间、更新时间、作者 ID。categories分类表存分类 ID、名称、创建时间。comments评论表存评论 ID、评论内容、用户 ID、文章 ID、父评论 ID用于嵌套回复、评论时间。article_tag / tags标签关联表因为文章和标签是多对多的关系所以需要一个中间表把两者关联起来。这里有一个很容易被忽视的设计细节文章正文到底存 Markdown 还是存 HTML我的建议是存 Markdown。原因很简单Markdown 格式文本在编辑阶段是纯文本可读性好、占用空间小渲染交给前端去做前端用 marked.js 或者 marked 库转换成 HTML 展示灵活度很高。万一你需要换主题、换展示方式源代码还在重新渲染即可。如果直接存 HTML后续样式调整就很痛苦。另外设计表结构的时候一定要注意软删除的问题。博客系统里如果文章删错了想恢复怎么办给表加一个is_deleted字段默认 0删除的时候更新为 1查询的时候默认过滤掉。这个设计在答辩时也是亮点会给老师留下你考虑得很周全的印象。3. 后端接口设计与核心逻辑实现后端是整个系统的大脑所有业务规则都在这层实现。Flask 的项目结构我建议按照模块去分不要把所有代码塞进一个app.py里。那种写法对小程序可以对于博客系统这种规模的项目后期维护起来会让人抓狂。3.1 推荐的项目目录结构blog_backend/ ├── app/ │ ├── __init__.py # 创建 Flask 应用注册扩展 │ ├── config.py # 配置文件数据库、JWT密钥等 │ ├── models/ # 数据库模型 │ │ ├── __init__.py │ │ ├── user.py │ │ ├── article.py │ │ ├── category.py │ │ └── comment.py │ ├── api/ # 蓝图Blueprint接口 │ │ ├── __init__.py │ │ ├── auth.py # 登录注册接口 │ │ ├── articles.py # 文章接口 │ │ ├── categories.py # 分类接口 │ │ └── comments.py # 评论接口 │ └── utils/ # 工具函数 │ ├── __init__.py │ ├── response.py # 统一返回格式 │ └── auth.py # JWT 鉴权装饰器 ├── migrations/ # 数据库迁移脚本 ├── venv/ # 虚拟环境 ├── run.py # 启动入口 ├── requirements.txt └── .env # 环境变量Blueprint 是 Flask 组织代码的核心方式它本质上就是把路由分组。比如所有跟文章相关的接口都是一个名为articles的蓝图里的路由。这有一点像 Excel 里的工作表标签页不同的数据放在不同的标签页里查找起来一目了然。config.py里应该集中管理配置信息比如import os class Config: SECRET_KEY os.getenv(SECRET_KEY, your-secret-key) SQLALCHEMY_DATABASE_URI os.getenv( DATABASE_URL, mysqlpymysql://root:passwordlocalhost:3306/blog_db ) SQLALCHEMY_TRACK_MODIFICATIONS False JWT_SECRET_KEY os.getenv(JWT_SECRET_KEY, your-jwt-secret)配置和代码分离是一种好习惯这样你换环境、换数据库连接时都不用改代码改环境变量即可。这也是企业开发的标准做法放在简历上能加分。3.2 统一返回格式与异常处理前后端分离的项目最忌讳的就是接口返回格式五花八门。一会儿返回一个数组一会儿返回一个字符串前端解析的时候就会崩掉。所以第一步就要定义统一的返回格式。我习惯用这样的 JSON 结构{ code: 200, message: success, data: { ... } }比如文章列表接口code200表示成功data里面放文章列表和分页信息{ code: 200, message: success, data: { list: [...], total: 100, page: 1, pages: 10 } }如果出错呢比如文章不存在就返回code404message文章不存在或已删除。这样前端拿到code一眼就能看出是什么问题不用去猜。实现上我会封装一个工具函数def success(dataNone, messagesuccess): return jsonify({code: 200, message: message, data: data}) def error(code400, messageerror): return jsonify({code: code, message: message, data: None})另外要注册全局的异常处理器避免数据库报错直接暴露给前端把原始的异常信息吞掉给用户一个友好的提示同时在服务端日志里记录下来。这对答辩时展示代码质量很有帮助。3.3 JWT 鉴权流程详解博客系统的登录接口原理是这样的用户提交用户名和密码后端校验通过后生成一个 JWT 令牌返回给前端。前端把令牌存储在 localStorage 或者 Vuex/Pinia 中每次请求时在请求头Header里带上Authorization: Bearer token后端验证通过后才允许访问受保护的接口。bp.route(/login, methods[POST]) def login(): data request.get_json() username data.get(username) password data.get(password) user User.query.filter_by(usernameusername).first() if not user or not check_password_hash(user.password_hash, password): return error(400, 用户名或密码错误) token create_access_token(identitystr(user.id), additional_claims{role: user.role}) return success({token: token, username: user.username, role: user.role})注意这里的密码千万不能明文存储一定要用werkzeug.security提供的generate_password_hash和check_password_hash来处理。哪怕被拖库了攻击者拿到的也是哈希值破解成本会高很多。这件事说一百遍都不为过。JWT 令牌里还可以带上额外信息比如用户角色。这样后端在解析 token 时就能顺手知道调用者是不是管理员加快权限判断速度。有一点要注意JWT 是无状态的签发出去之后在过期前是无法主动作废的所以过期时间不要设置太长正常情况 24 小时内比较合适。3.4 文章接口的列表与详情实现文章列表接口需要考虑过滤、分页、排序三个维度。我用 Flask-SQLAlchemy 的paginate方法配合条件过滤来实现bp.route(, methods[GET]) def get_articles(): page request.args.get(page, 1, typeint) per_page request.args.get(per_page, 10, typeint) category_id request.args.get(category_id, typeint) keyword request.args.get(keyword, ).strip() query Article.query.filter_by(statuspublished, is_deletedFalse) if category_id: query query.filter_by(category_idcategory_id) if keyword: like_pattern f%{keyword}% query query.filter( db.or_(Article.title.like(like_pattern), Article.summary.like(like_pattern)) ) query query.order_by(Article.is_top.desc(), Article.created_at.desc()) pagination query.paginate(pagepage, per_pageper_page, error_outFalse) articles [] for article in pagination.items: articles.append(article.to_dict()) return success({ list: articles, total: pagination.total, page: pagination.page, pages: pagination.pages })接口里要尽量只返回前端需要的数据不要直接把 ORM 对象全量返回避免把一些不该暴露的字段泄露出去。比如说文章的content在列表页只需要摘要就够了返回完整正文既浪费流量又拖慢响应速度。所以最好是给模型写一个to_dict()方法定义好哪些字段可见def to_dict(self): return { id: self.id, title: self.title, summary: self.summary, cover: self.cover, category: self.category.name if self.category else None, tags: [tag.name for tag in self.tags], read_count: self.read_count, created_at: self.created_at.strftime(%Y-%m-%d %H:%M:%S) }文章详情接口与之类似但要包含完整的 Markdown 正文。同时每次访问详情页时阅读量要加一。这里的细节是阅读量加一不要同步等待数据库写入完成再返回否则高并发下接口会很慢。可以在返回之后后台异步更新或者直接在前端加载时单独调一个增加阅读量的接口。对于毕设系统最简单的方式就是先查询返回数据然后执行article.read_count 1再db.session.commit()注意提交失败也别影响主流程包一层异常处理即可。3.5 评论功能的嵌套结构设计评论模块是博客系统里稍微有点复杂的功能。因为要支持楼中楼回复所以评论数据结构是嵌套的。这时候数据库里预留的parent_id字段就起作用了。查询评论时先查出所有parent_id null的顶层评论然后遍历每个顶层评论找到它下面的子评论递归组装成树状结构def build_comment_tree(comments): tree [] comment_map {comment.id: comment for comment in comments} for comment in comments: comment.children [] if comment.parent_id is None: tree.append(comment) else: parent comment_map.get(comment.parent_id) if parent: parent.children.append(comment) return tree前端拿到这棵评论树用递归组件去渲染就能展示出层叠的回复效果。这里我要提醒一句查询评论时不要搞 N1 查询如果先查顶层评论再逐个查子评论文章评论多了以后接口会非常慢。应该一次性把该文章的全部评论查出来在内存中组装树。评论量几千条以内时这个方案的性能毫无压力而且代码写起来也不复杂。树形结构的组装要注意一个边界场景如果父评论被删了子评论就会变成孤儿节点。我的处理方式是父评论被删时子评论保留但显示为回复已删除的评论或者直接提示原评论已被删除。3.6 Flask 后端部署环境配置项目开发完最终是要能跑起来给人看的。很多同学在本地跑得好好的部署到云服务器上就各种问题。我这里直接给出可行的部署流程。第一步服务器上安装 Python、MySQL、Nginx创建数据库和账号。第二步把代码上传到服务器用 git 拉取或者直接scp传都行。第三步在项目目录里创建虚拟环境安装依赖python3 -m venv venv source venv/bin/activate pip install -r requirements.txt第四步初始化数据库flask db init flask db migrate -m init tables flask db upgrade第五步用 gunicorn 启动后端服务gunicorn -w 2 -b 127.0.0.1:5000 run:app这里要注意Flask 自带的开发服务器无法承受生产环境的访问量而 gunicorn 是多进程的能更好地利用服务器资源。最后一步用 Nginx 做反向代理把前端构建出来的静态文件目录指向根路径把/api/的请求代理到127.0.0.1:5000server { listen 80; server_name your_domain; root /var/www/blog_frontend/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }这里面的try_files配置非常重要因为 Vue Router 用的是 history 模式直接访问某个路由路径比如/article/1时Nginx 找不到对应的物理文件就会返回 404。try_files $uri $uri/ /index.html的意思是找不到文件就回退到index.html由前端路由接管。这一步不做前端刷新页面就会白屏报错属于部署时最经典的坑之一。4. 前端页面的设计思路与 Vue 代码实现有人说前端不就是照着设计稿写页面吗其实没那么简单。前后端分离项目里前端的核心工作是把后端给的数据组织成页面结构并处理好用户的交互状态。好的前端代码能显著提升弹性和体验。4.1 前端项目结构和基础路由配置我用的 Vue 3 Vite Pinia Vue Router。项目结构如下blog_frontend/ ├── dist/ # 构建产物 ├── public/ ├── src/ │ ├── api/ # 封装 Axios 请求 │ │ ├── auth.js │ │ ├── article.js │ │ ├── comment.js │ │ └── category.js │ ├── assets/ │ ├── components/ # 公共组件 │ │ ├── CommentItem.vue │ │ ├── Pagination.vue │ │ └── MarkdownRenderer.vue │ ├── views/ │ │ ├── Home.vue │ │ ├── ArticleDetail.vue │ │ ├── CategoryView.vue │ │ ├── Login.vue │ │ ├── Register.vue │ │ └── admin/ │ │ ├── AdminLayout.vue │ │ ├── ArticleEdit.vue │ │ ├── ArticleList.vue │ │ └── Dashboard.vue │ ├── router/index.js │ ├── stores/user.js │ ├── App.vue │ └── main.js ├── index.html ├── package.json └── vite.config.js路由配置里需要区分前台路由和后台路由。后台管理界面应该加一个全局守卫检查用户是不是管理员不是就跳回首页防止普通用户直接通过 URL 访问后台。router.beforeEach((to, from, next) { const userStore useUserStore() if (to.meta.requiresAuth !userStore.isLoggedIn) { next(/login) } else if (to.meta.requiresAdmin userStore.role ! admin) { next(/) } else { next() } })4.2 Axios 请求拦截与响应处理前端每一次请求后端接口都要经过 Axios。我们需要在 Axios 的请求拦截器里加一个公共逻辑从 Pinia 中取出 token然后设置成请求头service.interceptors.request.use((config) { const userStore useUserStore() if (userStore.token) { config.headers[Authorization] Bearer ${userStore.token} } return config })响应拦截器里要统一处理几种情况 如果 response 里的code不是 200说明业务出错用 UI 组件弹个提示如果是 401说明 token 过期自动清除本地登录状态并跳转到登录页。统一处理的好处是每个页面里写请求逻辑时可以很清爽不用担心重复处理错误。service.interceptors.response.use( (response) { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, (error) { if (error.response error.response.status 401) { const userStore useUserStore() userStore.logout() router.push(/login) } ElMessage.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } )4.3 文章列表页与详情页的渲染要点文章列表页是博客的脸面设计上力求简洁清爽。每一张文章卡片一般包括封面图、标题、摘要、发布日期、阅读量、所属分类。数据从哪个接口来就是我们后端实现的文章列表接口。页面加载时调用一次点击分页时再调用一次用响应式数据接收结果。script setup import { ref, onMounted } from vue import { getArticles } from /api/article const articles ref([]) const total ref(0) const page ref(1) const loading ref(false) async function loadArticles() { loading.value true try { const data await getArticles({ page: page.value }) articles.value data.list total.value data.total } finally { loading.value false } } onMounted(loadArticles) /script文章详情页相对复杂一些既要展示 Markdown 渲染后的正文又要处理评论。Markdown 渲染我用的是marked库配合highlight.js实现代码高亮。正文从后端接口拿到 Markdown 字符串之后在模板里这样渲染template div classarticle-content v-htmlrenderedContent/div /template script setup import { computed } from vue import { marked } from marked import hljs from highlight.js const props defineProps({ content: { type: String, default: } }) const renderedContent computed(() { marked.setOptions({ highlight(code, lang) { if (lang hljs.getLanguage(lang)) { return hljs.highlight(code, { language: lang }).value } return hljs.highlightAuto(code).value } }) return marked.parse(props.content) }) /script这里有一个操作细节值得注意直接用v-html渲染用户输入内容存在 XSS 漏洞风险。如果用户的 Markdown 内容里嵌了恶意脚本就会被浏览器执行。Markdown 转换 HTML 时并不会自动过滤掉危险标签。所以如果你在评论区也允许用户写 Markdown一定要加上消毒处理用dompurify库过滤掉script等危险标签再插入页面。这是安全底线。评论区我的方案是递归组件。组件CommentItem.vue接收一个评论对象如果有子评论就再渲染自己。这个递归思想是理解嵌套评论展示的核心。4.4 后台管理界面的设计思路后台管理界面要有别于前台的设计。前台讲究美观后台讲究效率。常见的布局是左侧侧边栏、右侧内容区。侧边栏放菜单仪表盘、文章管理、分类管理、评论管理、用户管理。内容区根据路由变化展示对应页面。文章编辑页面是后台管理的核心需要把写博客的体验做到顺滑。我用的方案是双栏编辑左边是 textarea 写 Markdown右边实时预览渲染效果。一个简单但好用的技巧是把两个面板的 scroll 同步起来让用户在长文本滚动时能对应上左右位置。对于封面上传、图片上传本质上就是调用一个文件上传接口后端用 Flask 接收文件、保存到服务器的 uploads 文件夹里然后把访问 URL 返回给前端。图片存储方式对毕设来说存本地磁盘就够了不需要接云存储服务。这里有个容易被问倒的细节文件上传接口必须做类型白名单校验只允许.jpg,.png,.gif,.webp等图片格式防止用户上传可执行文件到服务器上造成安全隐患。同时文件名字不要直接用用户上传时的原始文件名一是可能有中文路径问题二是可能重复覆盖三是可能存在路径穿越漏洞。统一用uuid生成新文件名安全省事。5. 开发过程中最常见的坑与排查方法前端后端联调的时候问题千奇百怪我把实际指导别人做毕设时遇到的高频问题整理成一张速查表你照着排查就行问题现象可能原因排查与解决办法前端请求接口 404后端路由没注册 Blueprint或者路径前缀不对打开浏览器开发者工具看 Network 里请求的具体 URL和后端的bp.route()路径逐一对比前端请求接口 500后端代码报异常最常见是数据库字段名错误看后端终端日志找到 Python traceback 堆栈顺着第一行报错信息定位跨域问题请求被 CORS 拦截前端和后端不在同一个端口/域名没有配置跨域后端安装 flask-cors 并配置CORS(app)或者在 Nginx 上配好反向代理让两者同源前端显示无法获取 tokenPinia 的状态刷新后丢失因为默认存内存把 token 持久化到 localStorage在 store 初始化时从 localStorage 恢复到 state刷新页面 404Vue Router 用了 history 模式但服务器没有配置 fallback在 Nginx 配置try_files $uri $uri/ /index.html;图片上传成功但前端无法访问后端没有配置静态文件目录访问 404Flask 中用app.send_static_file或者添加 static_folder 的配置确认图片 URL 路径正确文章详情页切换文章时页面内容没有更新组件复用了Vue 没触发重新渲染在详情页的根组件上加:keyroute.fullPath强制切换路由时重新创建组件数据库中文乱码数据库连接字符串没指定 utf8 编码连接串里加上?charsetutf8mb4建库是指定 utf8mb4 字符集markdown 渲染出来的代码没有颜色highlight.js 样式没引入在入口文件里import highlight.js/styles/atom-one-dark.css5.1 跨域问题的最优解不是 CORS而是 Nginx 代理很多同学一碰到跨域就想到装flask-cors库这没错但它本质上是给响应头加了一些字段让浏览器放行。这种方式有两个缺点一是所有请求都要经过浏览器预检请求OPTIONS多一次往返二是安全性稍差因为你把跨域策略放宽了。最优解是部署阶段用 Nginx 做同源代理。前端请求的时候统一请求/api/xxxNginx 一看这个路径就转发到后端127.0.0.1:5000。对浏览器来说请求的是同一个域名就不存在跨域问题了。开发环境因为有 Vite 自带的 proxy 配置也不需要 CORS。// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://127.0.0.1:5000, changeOrigin: true } } } })这个方案最干净连后端的 CORS 配置都可以省掉。答辩时如果老师问起你就可以把这套原理讲清楚分数这不就上去了。5.2 那些看着没问题但就是不对的代码陷阱我给你报几个我自己写代码时容易出鬼的问题精确到每一行代码。第一Flask 的request.args.get默认值是字符串。如果不做类型转换request.args.get(page, 1)拿到的是1而不是1跟数据库分页时就会出幺蛾子。正确写法是request.args.get(page, 1, typeint)让它自动转换。第二SQLAlchemy 的filter_by和filter不用混着写。filter_by只能做等值判断filter可以写任意条件。在同一段查询里混用容易写出混乱的代码我推荐统一用filter可读性更强。第三提交事务后ORM 对象的外键属性没有立刻更新。你没看错创建的关联对象在 commit 之后可能仍无法立刻填充自增 ID这尤其容易在创建文章并添加标签时踩坑。解决办法是先db.session.flush()刷新一下拿到自增 ID再继续组装关联关系最后再 commit。第四前端列表删除最后一条数据后当前页变成空页。这个问题很隐蔽第 3 页只有 1 条数据删完之后 total 变成了上一页的总数page 还没变导致请求到空数据。解法是删除成功之后判断当前列表是否为空为空且 page 1 就把 page 减一再重新请求。5.3 我推荐你故意在毕设里加的几个小功能不加太多工作量但能让系统显得更完整、答辩更有底气文章置顶功能在管理员发布文章时勾选置顶的文章会排在所有普通文章前面。后台只需要一个布尔字段is_top排序逻辑加一句order_by(is_top.desc())就搞定。阅读量统计每次点进详情页 1。只加一个字段但数据可视化时很有用。标签云在侧边栏显示所有标签字体大小根据文章数量变化。稍微写点前端逻辑效果却非常出彩。RSS 订阅如果你还想炫技可以用 Flask 生成 RSS 订阅源feed.xml这是博客系统的经典功能老派但很唬人。按年月份归档在个人主页按年份折叠月份展示该月发布文章数。SQL 的DATE_FORMAT(created_at, %Y-%m)加group by就行。这些功能都不难但对答辩来说功能点丰富是一个天大的优势。老师一看哟阅读量、置顶、标签云、归档都有这个工作量一个抵三个自然就得高分。6. 文档、答辩与一条龙的那些事很多同学低估了文档的重要性。代码写得再好文档一塌糊涂答辩照样翻车。按照毕设要求文档一般要包含这些部分课题背景和意义、国内外研究现状、系统需求分析功能需求、非功能需求、系统设计架构设计、功能模块设计、数据库设计、系统实现、系统测试、总结与展望。写文档的关键是把你做了什么讲清楚尤其是系统架构图和数据库 ER 图这两张图基本上决定了老师对你的第一印象。工具不用高深在线的 draw.io 画好导出即可避免用复杂的工具把自己绕晕。答辩时最常被问的问题是为什么选 Flask 和 Vue不选别的数据库表是怎么设计关联的前后端数据交互是怎么实现的如果不做前后端分离会有什么问题系统有什么可以改进的地方别看这些问题简单回答不好照样扣分。所以讲代码的时候每个人都要有自己的思考。在改进方向里面你可以说将来可以做全站 HTTPS、可以做缓存层比如 Redis、可以把图片迁移到云存储、可以引入搜索引擎如 Elasticsearch做全文检索、可以做服务端渲染SSR提升 SEO。这些术语点到为止但能看出来真的有思考。我这里也提一下一条龙定制。很多同学时间紧或者代码能力有限就会找别人帮做毕设或者做技术指导。我的观点是你可以借助别人的代码省时间但一定要自己把代码逻辑读明白。否则答辩时老师往深了问几句答不上来反而更麻烦。拿到一份现成代码后我的建议是先把项目在本地完整跑一遍把每个功能都点一遍。然后看着代码画一张功能与代码文件对应表知道每个功能在哪个文件、哪一行实现。最后自己动手改几个小地方比如改页面的标题、改文章列表每页显示数量、改一处样式让系统有一点点你的痕迹。这么做既节约了开发时间又确保了答辩时不会头脑空白。不管你最终的技术实现是自己写的还是参考来的能讲清楚原理才是真正的目的。7. 部署上线后还可以继续玩的扩展方向如果博客系统已经跑通了答辩时间也还充裕有几个方向可以让系统更上一层楼。第一个方向是接入 Redis 缓存。文章详情页是被读取最多的接口可以把热点文章的 Markdown 原文放到 Redis 里设置过期时间比如 10 分钟过期后从数据库重新加载并刷新缓存。原理讲起来并不复杂把一份经常读取的数据放在内存里减少对数据库的查询。第二个方向是文件上传走七牛云或者阿里云 OSS。本地盘的瓶颈在于磁盘大小和备份麻烦云存储的 SDK 接入也很简单申请存储空间、配置密钥、上传文件、返回 URL差不多一两天就能改完。答辩时提一嘴系统支持将图片文件存至第三方云存储可横向扩容就会显得容量很大。第三个方向是用 Docker 容器化部署。写一个Dockerfile和一个docker-compose.yml把 Flask 后端、MySQL 数据库、Nginx 前端都做成三个容器docker-compose up -d一条命令启动整个系统。这已经属于加分大项了因为现在很多企业都在用容器化你提前接触了意识就很超前了。第四个方向是把文章导入/导出做成 Markdown 文件。加一个批量导入接口前端可以拖拽一组.md文件进来后端逐个解析并存入数据库。这个功能特别适合内容迁移的场景——你以前在其他平台写的文章可以一键导入新博客很实用的功能也很容易实现。最后还有个小技巧想分享给你。部署完成之后记得重点检查三个地方一是后端的日志用 gunicorn 启动时加上--access-logfile -就可以实时看到每个请求的日志排查404、500很有用二是服务器时间如果服务器时区不对文章发布时间就会显示错 8 个小时这个要在数据库连接串和系统时区上都要设置成Asia/Shanghai三是上传目录的读写权限很多同学搞了三天发现图片传不了结果就是目录权限没改。做毕设是一个把课堂知识真正落地成完整系统的过程中间会有很多坑也会有很多哦原来是这样的时刻。这个博客系统做完你不仅能交付一份合格的毕设更重要的是你具备了一个全栈开发者的基本素养会设计数据库、会写后端接口、会写前端页面、会部署、会调优。这套技能放在简历上拿出手也完全不虚。