ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

全栈博客系统实战:Vue3+SpringBoot+MySQL+Redis从设计到部署

全栈博客系统实战:Vue3+SpringBoot+MySQL+Redis从设计到部署 做全栈开发的几乎都绕不过博客系统这个项目。社区里说自己是全栈工程师的人十有八九第一个拿得出手的作品就是它去面试聊项目讲博客系统也是最不容易冷场的开场白。这个项目看起来简单——发文章、看文章、删文章但真正动手做起来你会发现它把全栈开发的骨架全都碰了一遍数据库设计、接口规范、认证鉴权、富文本处理、文件上传、缓存、部署上线。说它是微缩版的内容管理系统毫不夸张麻雀虽小五脏俱全。这次我把一套完整的全栈博客系统从零到上线的全过程整理出来。技术栈选的是 Vue 3 Spring Boot 3 MySQL Redis前端用 Vite 构建后端按模块拆分最终用 Docker Compose 部署到一台云服务器上。整套代码量不算大但足够覆盖生产级个人博客需要的全部核心能力。不管你是刚学完框架想找个全栈项目练手还是已经在职想系统补齐全栈开发的知识盲区这篇内容都可以当作一份完整参考。先说清楚这篇文章会讲什么。我不会贴一整仓库的代码让你自己看而是把设计决策、关键实现、踩坑记录一条条拆开讲——为什么这样建表、为什么用 JWT 不用 Session、前端路由为什么这么组织、部署时 Nginx 怎么配才能让 SPA 刷新不 404。每个决策背后都有原因这些原因才是做全栈项目真正值钱的部分。项目做到后面你会发现写代码的时间只占三成剩下七成都耗在环境、配置和细节上。1. 动手前先把需求边界画清楚1.1 博客系统不仅仅是文章的增删改查很多人拿到博客系统这个需求第一反应就是打开 IDE 开始写 Article 表写一个新增接口再写一个列表页感觉很快就能跑起来。真这么做的人多半做到一半就开始返工因为博客系统牵扯的角色和场景比你最初想的要多。从用户视角拆分一个完整的博客系统至少要包含两个终端。普通访客看到的是前台页面能浏览文章列表、点进详情阅读、按分类和标签筛选文章、搜索关键词、发表评论内容管理者看到的是后台管理界面要登录、发文章、改文章、管理分类标签、审核评论。这两个视角对功能的要求完全不同前台要轻要快后台要全要顺手。很多新手失败就失败在把这两个职责混在一个页面里写。再往下拆文章本身也不是一张简单表就完事的。文章有状态草稿和已发布要分开文章有分类和标签分类是层级结构还是平铺结构标签是自由输入还是可复用文章内容要不要存格式编辑时用 Markdown 还是富文本评论要不要审核要不要支持嵌套回复图片上传是存本地还是对象存储。这些都是在写第一行代码之前就该定的问题。我的做法是先列用户故事再画功能清单最后砍掉三个没用的功能让核心路径保持干净。最初版本只保留文章管理、分类标签、评论展示、Markdown 渲染这四条主线其余功能全部放到迭代计划里。1.2 技术栈选型我为什么选了 Vue 3 Spring Boot技术栈的选择是这类项目里最容易被质疑的部分。我直接说结论这套博客用的是 Vue 3 Vite 做前端Spring Boot 3 MyBatis-Plus 做后端MySQL 8 存业务数据Redis 做缓存部署用 Docker Compose Nginx。这个组合不是最好的但它是目前中文技术社区里资料最全、最容易找到同类项目参考的组合。先说后端。选 Spring Boot 是因为它的生态太成熟了安全框架有 Spring Security 或者 Sa-TokenORM 有 MyBatis-Plus 和 JPA文件处理、参数校验、定时任务都有现成方案。对想系统学习后端的人来说Java 这条路线的就业面也广学了不亏。如果你完全不用考虑找工作只想快速把博客跑起来用 Node.js 的 Express 或者 NestJS 也完全可以代码量会更少。我见过用 Python FastAPI 做的也见过用 Go Gin 做的都能做得很好核心问题不在语言而在你对这个生态是否熟悉。前端锁 Vue 3 的理由也类似组合式 API 写业务逻辑比选项式清晰Vite 的开发体验比老一代 Webpack 构建快出一个量级。这里有一个要提前想清楚的权衡博客内容需要被搜索引擎收录纯 SPA 的 SEO 效果很差。我选的妥协方案是前台做 SPA但每个页面都设置完整的 title、description、keyword 和 Open Graph 标签文章详情页由后端提前渲染好 META 信息注入到 index.html 模板中。这个方案对个人博客足够用。如果追求极致的 SEO应该直接上 Nuxt 或 Next.js 做 SSR但对全栈学习来说那一套的复杂度会分散你学后端和部署的精力。1.3 模块划分与开发顺序模块划分的目标是让每个人都能在自己的脑子里建立清晰的上下文边界。我按部署单元和职责边界把项目切成了前端、后端、数据库、中间件四块前端内部再拆成 portal门户和 admin管理后台两个路由域后端内部按 controller、service、mapper、entity 四层组织。这个划分先于代码存在它决定了后续所有人写代码时把文件放到哪里也决定了 CI/CD 的产物是什么。开发顺序也有讲究。我在这个项目里的顺序是先定数据库表结构再出接口文档接着写后端代码然后写前端页面最后做部署。反过来做一定会后悔。因为表结构是数据的根基表设计错了接口和页面全都要跟着改接口文档是前后端协作的契约先定义好路径、参数、返回结构前端开发时就不用一边写一边猜后端返回的字段。这听起来像团队协作的流程但一个人做全栈项目时同样适用因为你会频繁地在前后端之间切换如果不先把契约定好切换时的上下文丢失成本非常高。实际开发时我给自己的节奏是第一周只做数据库设计和接口文档第二周完成后端全部核心接口第三周做后台管理页面第四周做前台浏览页面第五周部署上线。前两周是最枯燥的但后三周的顺利程度完全取决于前两周想得够不够清楚。2. 后端从建表到接口的一整套设计思路2.1 表结构设计五张核心表和一个多对多关系建表是整个项目最不能急的一环。我在第一版设计里定了五张核心表用户表、文章表、分类表、标签表、评论表外加一张文章标签关联表。下面把文章表和关联表单独拿出来讲因为它们最能代表设计取舍。文章表的核心字段长这样CREATE TABLE t_article ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, summary VARCHAR(500) NULL, content_md LONGTEXT NOT NULL, content_html LONGTEXT NOT NULL, category_id BIGINT NULL, cover_image VARCHAR(500) NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-草稿 1-已发布, view_count INT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted TINYINT NOT NULL DEFAULT 0, KEY idx_category_status (category_id, status), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;第一个容易纠结的问题是文章内容为什么要存content_md和content_html两列。我见过很多方案只存 Markdown 原文每次请求时在后端现渲染成 HTML。这个方案代码简单但有几个问题一是渲染逻辑每次请求都要执行CPU 开销不划算二是如果以后渲染规则升级你无法只对历史文章定向刷新三是后台保存为草稿时不方便预览最终的渲染效果。我现在每次保存文章时后端把 Markdown 渲染成 HTML 再一起落库查询时直接返回已经渲染好的内容接口响应速度肉眼可见地快。这是用一个字段的空间换取每个请求的时间对内容系统是划算的。第二个容易踩坑的点是软删除。为什么不用物理删除因为文章发布过就可能被搜索引擎收录、被外部引用物理删掉会导致死链评论更是需要保留审计痕迹。所以我在每张表上都加了deleted字段查询时统一加deleted 0条件。这里有个我自己踩过的坑MySQL 索引对deleted这样的低选择性字段帮助有限如果列表查询经常带status和category_id索引应该建在业务字段的组合上而不是单独给deleted建索引。我在文章表上建了(category_id, status)的联合索引配合分页查询效果不错。联合索引的字段顺序也很重要要把区分度高的字段放前面同时要考虑等值查询和范围查询的混合场景。分类和标签的关系也值得多说一句。分类适合用单表平铺一个文章只属于一个分类因为个人博客的分类数量一般不超过十个不需要做层级。标签则是典型的多对多场景一篇文章可以有多个标签一个标签下有多篇文章所以单独建t_article_tag关联表。关联表只需要三个字段主键、文章 ID、标签 ID然后给article_id建索引。这样做的好处是标签可以复用统计某个标签下的文章数量时直接查关联表效率和灵活性都远高于把标签存成逗号分隔字符串。逗号分隔的方案在处理修改文章标签标签合并按标签过滤时都很难受这是新手最容易忽略的地方。2.2 接口设计统一响应、JWT 认证与权限控制表结构定了接下来就是接口设计。我全部按 RESTful 风格来路径里只写资源名词操作用 HTTP 方法表达。核心接口整理出来是这样的方法路径说明权限POST/api/auth/login登录返回 token公开GET/api/auth/me获取当前登录者信息管理员GET/api/articles分页查询已发布文章支持分类、标签、关键词过滤公开GET/api/articles/{id}文章详情浏览量 1公开POST/api/articles新建文章管理员PUT/api/articles/{id}更新文章管理员DELETE/api/articles/{id}删除文章软删除管理员GET/api/admin/articles分页查询全部文章包含草稿管理员POST/api/comments提交评论公开GET/api/comments查询文章评论列表公开所有接口返回统一的结构{ code: 0, message: success, data: {...} }。code 是业务状态码0 代表成功非 0 代表各种错误比如 401 表示未登录、403 表示无权限、404 表示资源不存在。这个约定前后端都要遵循前端 axios 拦截器只看 codeHTTP 状态码只作为传输层信息。我见过不少项目把业务错误直接映射成 500前端一接到 500 就得猜后端哪里炸了体验很差。统一响应结构之后前端处理错误会清爽很多。认证方案我选了 JWT。对比 Session 方案JWT 是无状态的后端不存登录状态水平扩容时不用考虑 Session 同步的问题也天然适合前后端分离的场景。JWT 本身是个三段式字符串Header 存算法类型Payload 存用户信息和过期时间Signature 用服务端密钥对前两段签名防止篡改。登录成功后后端生成 token 返回前端保存起来之后每个请求都在Authorization: Bearer token头里带上后端用一个拦截器解析 token、取出用户信息放进上下文。我项目里的真实参数是Access Token 有效期 2 小时签发密钥放在环境变量里密码用 BCrypt 加密存储成本因子取 10。权限控制这一层我没有使用 Spring Security 全家桶原因是不想引入大量配置把核心逻辑淹没掉。我用了轻量的拦截器方案写一个AuthInterceptor在配置类里注册到需要鉴权的路径上比如/api/admin/**。拦截器里做三件事解析 token校验签名和过期时间检查用户角色是否为管理员。这三步全部通过才放行。对于学习项目来说这种方式从头到尾都可以自己控制理解起来也更直接。如果你的目标是进大厂面试还是建议系统学一下 Spring Security 的 Filter Chain 和授权模型原理是相通的只是配置复杂度更高。2.3 Markdown 渲染、XSS 清洗与代码高亮博客系统的核心是内容展示内容格式则决定了阅读体验。我这边文章内容一律用 Markdown 编辑、存储、渲染后台保存时由后端负责把 Markdown 转成 HTML再存储到content_html字段。这个方案需要解决三个问题渲染扩展、安全清洗、代码高亮。渲染扩展指的是 Markdown 语法之外的常见需求。基础 Markdown 没有表格、任务列表、删除线这些能力所以我用的是 GFM 风格即 GitHub Flavored Markdown它是在标准 Markdown 基础上扩展了这些语法。Java 生态里的实现很多我选的是 flexmark因为它对 GFM 支持完整扩展机制也灵活。另外一个需求是标题锚点文章内容里的一二级标题要自动生成 id这样前台页面才能实现目录跳转和文内链接。flexmark 提供了一个 AnchorLink 扩展配置一下就能在渲染时自动给标题加锚点。安全清洗是必须死磕的一环。Markdown 本身允许嵌入原始 HTML 标签如果渲染时不清洗用户可以往文章里写script标签发布后直接在前台执行这就是典型的存储型 XSS。文章是博主自己写的还好评论是用户生成的风险更大。我的做法是在渲染完成之后再用一个 HTML 白名单过滤器清洗一遍只保留 p、h1-h6、pre、code、img、a、ul、ol、li、blockquote、table 这些安全的标签script、iframe、object、style、form 等一律删除标签上的事件属性和 javascript: 链接全部剔除。Java 生态里常用的清洗库是 OWASP Java HTML Sanitizer配置一个白名单策略调用一次 sanitize 就行。这一步不能偷懒做全栈项目必须建立安全意识。代码高亮我放在前端做。后端渲染 HTML 时给code标签带上语言 class前端引入 highlight.js 或者 Prism 的主题样式页面加载后自动识别并着色。我个人更推荐 highlight.js配一个 GitHub 风格的深色主题和阅读页的整体风格比较搭。代码块的行号、复制按钮这些附加功能可以后续再加第一版先把着色和横向滚动做好。3. 前端工程化、路由与编辑体验3.1 项目初始化与目录规范前端部分我用了 Vue 3 Vite Vue Router Pinia 的组合。项目初始化命令很简单npm create vitelatest选 Vue 模板即可。真正的工作量在目录规范和基础封装上这些决定了项目写到后面会不会乱。我的目录结构大致是这样的src/api放接口请求模块每个资源一个文件比如article.js、comment.js、auth.jssrc/views下按照portal和admin两个目录划分路由组件src/components放公共组件比如分页器、文章卡片、评论列表src/stores放 Pinia 的状态定义src/utils放请求封装、时间格式化、文本截断这些工具函数。src 根目录下再放一个router/index.js和一个styles/目录。这套结构不复杂但边界很清晰任何人接手项目都能在三十秒内找到自己该改的文件。工程化配置里有两个点值得多说。第一是路径别名我把指向src目录这样深层组件里引用其他模块时不需要写一长串相对路径代码可读性好很多。Vite 需要在配置文件的resolve.alias里设置同时记得在 jsconfig.json 或 tsconfig.json 里同步声明否则编辑器跳转会失灵。第二是环境变量我在项目根目录建了.env.development和.env.production里面定义VITE_API_BASE_URL开发环境指向本地后端生产环境指向服务器的 HTTPS 域名。Vite 会自动把以VITE_开头的变量暴露给前端代码这样打包时不需要改任何业务代码环境切换全靠变量控制。3.2 路由守卫、状态管理与请求封装路由设计上我将所有带/admin前缀的页面放进一个单独的路由层级统一挂在AdminLayout组件下面这个布局组件负责渲染侧边栏、顶栏和内容区域。登录页在/login前台门户的首页在/文章详情在/article/:id分类和标签聚合页分别用/category/:slug和/tag/:name。这样一个干净的路由表配合导航守卫就能把权限控制的逻辑集中到一处。导航守卫是前端权限控制的关键。我在router.beforeEach里做了三件事判断目标路由是否以/admin开头如果进入管理后台检查 Pinia 的 user store 里有没有 token没有 token 就重定向到/login?redirect/admin登录成功后回跳。这里要特别注意一个问题token 存在 localStorage 里刷新页面后 Pinia 状态会清空但 localStorage 里的 token 还在。所以在 store 初始化或应用启动时要加一步从 localStorage 重新恢复用户状态的逻辑否则用户明明登录过刷新一下就又被踢到登录页。请求封装要说的细节最多。我基于 axios 写了一个统一实例baseURL从环境变量读取timeout设成 15000 毫秒。请求拦截器负责从 token 存储里取出 JWT放到请求头的Authorization字段。响应拦截器是重点先判断 HTTP 状态非 2xx 统一走错误提示再按业务 code 分流code 为 0 时直接返回response.data.data非 0 时弹出错误消息如果遇到 401说明 token 已过期这时要清空本地登录态、跳转到登录页。我在这段逻辑里踩过一个坑登录接口本身请求时还没有 token如果拦截器无脑加 Authorization 也没问题但 401 的处理逻辑要跳过一个白名单机制否则登录失败会触发无限循环跳转。3.3 编辑器选型和阅读页的体验细节后台用得最多的功能就是写文章编辑器的选择直接影响使用体验。我对比过三个方案纯 textarea 配合 Markdown 预览、引入 Vditor、引入 ByteMD。纯 textarea 方案代码最少但用户体验太原始需要左右分屏预览还得自己做工具栏实际用起来效率很低。最终我选了 Vditor因为它开箱即用支持即时渲染模式、所见即所得的工具栏、文件上传回调、代码高亮和深色主题。说实话它的包体积不算小但管理后台是登录后访问的对首屏加载的敏感度没那么高这个取舍可以接受。Vditor 的接入比想象中简单核心是两件事初始化传一个 div 元素设置初始内容和模式配置好上传回调让编辑器里的图片按钮能直接把文件传到后端。上传回调里要带Authorization请求头因为上传接口是管理员权限上传成功后后端返回图片 URLVditor 会自动把 URL 插入到光标位置。这里有个前后端约定要提前确认上传接口的响应格式必须是 Vditor 预期的{ code: 0, data: { url: xxx } }如果你用统一的接口封装返回{ code: 0, message: , data: { url: xxx } }那也是兼容的。阅读页的体验和编辑页完全不同核心目标是让读者舒服地看完一篇文章。我在文章详情页做了几件事标题和摘要排版用大字号和宽松行距目录导航放在页面右侧滚动时自动高亮当前章节代码块使用 highlight.js 着色并开启横向滚动图片懒加载用 loadinglazy 加占位背景色防止布局抖动。还有一个细节是阅读进度条监听 window 的 scroll 事件计算出已读百分比在页面顶部显示一条细进度线这个功能实现起来不到三十行代码但对阅读体验的提升非常明显。4. 部署上线Docker 编排、Nginx 与 HTTPS4.1 前后端容器化与 Compose 编排部署环节是全栈项目里最劝退新手的一步也是踩坑最多的一步。我的方案是前端和后端各写一个 Dockerfile数据库和 Redis 直接用官方镜像最后用一个 docker-compose.yml 把四个服务编排在一起。前端 Dockerfile 用多阶段构建第一阶段用node:18-alpine作为构建镜像安装依赖后执行npm run build产出 dist 目录第二阶段直接基于nginx:alpine把 dist 目录里的文件复制到 Nginx 的默认静态目录/usr/share/nginx/html。这个做法的好处是最终镜像里只有 Nginx 和静态文件体积只有几十兆而且构建产物是高度可复现的。后端 Dockerfile 思路一样第一阶段用maven:3.9-eclipse-temurin-17执行mvn clean package第二阶段用eclipse-temurin:17-jre作为运行环境把 jar 包复制进去启动命令是java -jar。多阶段构建最大的价值是运行镜像里不会残留编译器、依赖缓存这些无关文件既缩小镜像体积也降低被攻击的面。docker-compose.yml 是部署的核心配置文件。MySQL 和 Redis 挂载了数据卷保证容器重建后数据不丢MySQL 设置了初始数据库名和账号密码后端服务通过环境变量注入数据库连接串、Redis 地址和 JWT 密钥这些敏感信息放到一个.env文件里.env不进 Git 仓库。服务间的通信走 Docker 内部网络后端访问数据库和 Redis 直接用服务名当主机名不需要暴露这些端口到公网。前端 Nginx 容器把 80 和 443 端口映射到宿主机反向代理后端 API。这里要提醒一句容器内部的服务端口不要随意映射到宿主机尤其是 MySQL 的 3306暴露到公网等于给黑客送靶子。4.2 NginxSPA 路由刷新 404 与反向代理Nginx 配置是全栈博客上线时最容易出问题的部分。第一个问题就是 SPA 路由刷新 404。Vue Router 默认用 history 模式路由路径是真实的 URL比如/article/123。前端容器里其实并没有article/123这个物理文件当用户在浏览器里直接访问这个地址或者在这个页面按 F5 刷新Nginx 会去找对应的文件找不到就返回 404。解决办法是配置 try_fileslocation / { try_files $uri $uri/ /index.html; }这个配置的意思是先尝试查找请求的 URI 对应的文件找不到就尝试找目录目录也找不到就回退到index.html由 Vue Router 接管并渲染对应的页面。这一行配置是 SPA 部署的标配不知道的人第一次上线几乎都是在这里卡住的。第二个问题是反向代理。前端容器和后端容器在同一个 Docker 网络里但外部浏览器只能访问 Nginx 的 80 端口。所有以/api开头的请求都要由 Nginx 转发到后端容器去。我给 Nginx 加的配置是location /api/ { proxy_pass http://backend:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }注意proxy_pass后面如果带了路径转发时会替换匹配到的前缀我这里不带路径请求原样转发。backend:8080这个地址是 Docker Compose 里后端服务的服务名和端口在容器内部网络里可以直接用服务名访问不需要知道容器的动态 IP。这套配置跑通之后前端的VITE_API_BASE_URL在生产环境里就写/api这一个相对路径不需要写完整域名浏览器发请求时自动打到同域的 Nginx 上再由 Nginx 转发到后端同域请求天然没有跨域问题。HTTPS 是上线博客必须要做的。理由很简单搜索引擎对 HTTPS 站点有倾向性浏览器对 HTTP 请求会显示不安全警告而且 JWT 在明文 HTTP 下传输等于裸奔很容易被中间人截取。我用 Certbot 申请了免费的 Lets Encrypt 证书把 80 端口请求 301 跳转到 443证书配置好后加一个定时续期任务。证书相关的文件和配置路径很多建议把证书放在独立的目录用 volumes 挂载进 Nginx 容器续期时从宿主机执行脚本即可。4.3 缓存策略Redis 缓存与静态资源加速部署上线后博客的性能优化才真正开始。个人博客流量不大但体验必须快慢一秒都对不起读者。我的优化分三层数据库层、Redis 层、静态资源层。数据库层最基础的是索引优化。文章列表页按发布时间倒序、按分类过滤我在(category_id, status)上建了联合索引评论按article_id查询单列索引就够了。不要迷信索引越多越好索引会拖慢写入速度、占用磁盘空间只给真正的查询路径建索引。每次写完一条新查询我习惯先EXPLAIN看一眼执行计划确认 type 不是 ALL 全表扫描再考虑下一步。Redis 层我主要做文章详情的缓存。详情页是访问量最大的页面而且文章内容几乎不变非常适合缓存。我用 Cache Aside 模式查询时先读 Rediskey 是article:detail:{id}命中直接返回没命中就查数据库查到后写入 Redis设置十分钟过期时间。后台修改文章时除了更新数据库还要主动删掉对应 Redis key保证下次请求是新鲜数据。这个模式实现成本低收益立竿见影。列表接口也可以缓存但因为分页参数变化多缓存粒度不好控制个人博客流量不大列表直接查数据库加索引就够了不要过度设计。静态资源层Nginx 对 dist 目录里的 JS、CSS、图片资源配置了强缓存。Vite 构建时会给文件名带上 hash 后缀内容变了文件名就变所以这些资源可以放心设置expires 7d和Cache-Control: public, max-age604800浏览器直接用本地缓存连请求都不发。图片资源如果以后量大可以再接入对象存储加 CDN但个人博客初期用本地存储配合 Nginx 静态托管足够了。别忘了给 Nginx 开启 gzip一般文本资源的压缩率在 60% 以上一个 200KB 的 JS 文件能压缩到 80KB 左右传输时间直接砍半。这些优化做完之后本地的 Lighthouse 性能评分基本稳定在 90 分以上。5. 常见问题排查与实战避坑记录5.1 跨域和登录态开发期与生产期的两套体验前后端分离项目碰到的第一个高频问题一定是跨域。开发时前端跑在 Vite 的 5173 端口后端跑在 8080 端口浏览器里前端页面所在域是http://localhost:5173它去请求http://localhost:8080/api就是跨域。解决跨域有三个常用方案我建议用 Vite 代理而不是在后端开 CORS。Vite 代理的做法是在vite.config.js里配置server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端开发时请求/api开头的接口Vite 会代替浏览器转发到后端浏览器端看到的是同域请求不存在跨域问题。这个方案的好处是不动后端代码生产环境里 Nginx 也是同样的代理逻辑前后端环境保持一致开发和生产之间几乎没有行为差异。如果不方便用代理也可以在 Spring Boot 里配 CORS 过滤器允许指定源跨域访问。我必须提醒一件事跨域配置里如果用了allowedOrigins(*)同时又要带 Cookie 或凭证浏览器会直接拒绝因为 Provisional headers 的问题会让你排查半天。正确的做法是把允许的来源明确写出来比如http://localhost:5173而不是用通配符。登录态失效是另一个必踩的坑。JWT 过期后用户正在后台写一篇长文章点保存时返回 401前端直接把人踢到登录页文章内容还没保存气得摔键盘。我的改进方案是在 axios 响应拦截器里遇到 401 先不立刻跳转而是尝试用 refresh token 换新的 access token刷新成功就重放刚才失败的请求用户无感刷新失败才清空登录态并跳转登录页。因为刷新 token 可能并发触发多次我加了一个变量标记刷新状态刷新期间其他请求先挂起排队等新 token 拿到后统一重放。这套逻辑实现起来约五十行代码但对体验的提升是决定性的。5.2 图片上传文件校验、存储路径与访问映射图片上传在博客系统里是躲不开的。用户写文章要插图设置封面要传封面这些文件校验做不好服务器迟早要出事。我在这上面踩过很深的坑现在总结出三条铁律。第一文件类型不能只看扩展名。用户把文件名改成xxx.jpg但内容实际是个 PHP 脚本你的校验如果只检查扩展名等于开了个后门。至少要做两层校验先看 Content-Type再读取文件头几个字节的魔数比如 JPEG 的开头是FF D8 FFPNG 的开头是89 50 4E 47GIF 是47 49 46 38。Java 里可以用一个简单的工具方法读取文件头只有魔数匹配才放行。第二文件名一定不能使用用户上传的原始文件名。原始文件名可能存在路径穿越风险也可能包含中文和特殊字符导致乱码。我的做法是用 UUID 生成新文件名但保留原始扩展名再按日期分成子目录比如/uploads/2025/06/01/uuid.jpg。这样文件名已知且唯一管理也方便。第三请求体和代理都要限制大小。我在 Spring Boot 的配置文件里设置了spring.servlet.multipart.max-file-size10MB同时 Nginx 的client_max_body_size也要设置成同样的值否则大的上传请求会先被 Nginx 拦下来后端看到的是 413排查时方向很容易跑偏。图片存储位置我第一版用本地磁盘放在后端项目的静态目录下通过 Nginx 的/uploads/location 直接映射访问。后来流量大了、需要换服务器时才发现这种方案的迁移成本很高图片和代码耦合在一起备份时容易漏。如果你预期图片量会涨建议一开始就设计成独立的文件访问路径或者直接接入对象存储。第一次做项目时可以先本地存储跑通全流程但心里要有数这不是终局方案。5.3 数据备份、时间时区与编码问题最后这段写几个看起来不起眼、实际上能坑到人的细节。第一个是数据库备份。线上博客最怕的不是崩溃而是数据丢了找不回来。我配了一个简单的定时任务每天凌晨三点用 mysqldump 把整个库导出成 SQL 文件压缩后保留最近七份再同步一份到另一台服务器上做异地容灾。恢复流程我也实际演练过在干净的 MySQL 容器里导入 SQL 文件检查文章量、评论量、账号信息是否一致确认恢复时间点。备份这件事做了不一定有事不做一定出事而且出事一定是在你没有备份的那一天。第二个是时区问题。MySQL 连接串上如果不加serverTimezoneAsia/Shanghai会跟你服务器的系统时区产生偏差Java 17 里如果数据库字段是 DATETIME而实体字段用了LocalDateTime读写时也要注意时区换算。我的建议是所有时间字段在数据库里统一用 DATETIME 存本地时间后端用 LocalDateTime 映射前端展示时再根据读者所在时区做一次格式化。千万别把时间戳和字符串混着存查起来你会疯掉。第三个是字符集问题。文章的 Markdown 内容里有 emoji、有各种中文标点如果表字符集不是 utf8mb4部分字符写入时会被截断或变成问号这是字符集最强的受害者。MySQL 8 里默认字符集已经是 utf8mb4但如果你是老版本迁移来的记得检查每个表和每个字段的字符集。建表时我统一指定CHARSETutf8mb4并在连接串里加上characterEncodingutf8从源头杜绝乱码问题。这套博客系统做完之后我自己最大的感受是全栈项目最难的从来不是某个技术点而是把散落的细节串成一个完整闭环的能力。数据库、后端、前端、部署每一层单独拎出来都有大量资料可查但它们之间的衔接——比如 Markdown 渲染放后端还是前端、token 存哪、图片怎么访问、备份怎么恢复——这些决定恰恰是网上很难直接找到答案的部分。我的建议是第一版一定要克制按本文这个规模做先把闭环跑通再根据实际使用反馈去迭代功能。如果你遇到和我不同的坑欢迎记录下来补进自己的项目文档这些一手经验比任何教程都值钱。
返回列表