news 2026/10/17 3:08:37

前后端分离博客系统实战:SpringBoot+Vue+MyBatis全栈部署指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
前后端分离博客系统实战:SpringBoot+Vue+MyBatis全栈部署指南

前后端分离这个词,在个人项目里被说烂了,但真正把这套架构跑通、部署上线、还能在日常使用中顺手维护的博客系统,其实没那么多。我这个项目用的是 SpringBoot + Vue + MyBatis + MySQL 这套组合,前后端完全分离,源码和部署过程都完整走了一遍。这篇文章不是课程文档,是我做完整个项目之后的总结——包括我为什么选这套技术栈、数据库和接口怎么设计、前后端各自的核心代码长什么样、本地怎么跑通、服务器上怎么部署,还有那些我踩过之后才知道的坑。如果你正打算做一个能真正上线、可长期维护的个人博客系统,这篇应该对你有用。

1. 先说说我为什么选这套组合做博客

1.1 从单体到前后端分离:我经历过什么

以前做个人博客,我一开始用的是典型的单体架构——Thymeleaf 模板直接渲染 HTML,后端既要处理数据又要输出页面。当时觉得挺爽,写起来确实快,一个 Controller 里既查数据库又拼页面,半小时就能出一个列表页。

但用久了问题就来了:页面稍微调个样式,后端得重新打包重启;想加个异步加载的效果,模板引擎那套机制始终隔了一层;更别说换一个前端框架的时候,基本等于把整个展示层推倒重来。后来我把同一个项目拆成前后端分离的架构,才发现之前那些"小问题"其实都是架构层面带来的限制。前后端分离之后,后端只需要返回 JSON 数据,前端负责渲染和交互,两边各自独立开发、独立部署,改页面不需要碰 Java 代码,换主题也只是换一套前端的事。

这次做个人博客系统,我之所以坚持用前后端分离而不是再走单体老路,核心原因就是:博客这种内容型产品,改主题、调布局的频率远高于改业务逻辑。与其每次都被模板引擎绑架,不如一开始就把展示和逻辑拆干净。

1.2 每个技术选型背后的理由

技术选型这块,我见过不少人纠结。说实话,个人博客系统的业务复杂度摆在那里,根本没有所谓的"最优解",只有"最顺手"的方案。我选 SpringBoot + Vue + MyBatis + MySQL,每一个都是基于实际需求权衡过的。

SpringBoot 不用多说,它解决了 Spring 框架最让人头大的配置问题,内嵌 Tomcat,打出一个 jar 包就能跑。对于一个博客系统来说,SpringBoot 自带的 starter 机制可以非常方便地集成 Web、MyBatis、安全校验等模块,开发效率很高。

Vue 这边,我选的是 Vue 2 的生态。不是说 Vue 3 不好,而是在当前这个项目里,Vue 2 + Element UI 的组件体系相当成熟,网上的踩坑资料也够多。前后端分离博客的前端复杂度不高,路由、状态管理、组件通信这三件事用 Vue 2 处理起来都很直接。如果你是从零开始学,Vue 2 的上手成本也确实低一些。

MyBatis 和 MySQL 这一层,属于经典组合。MyBatis 的灵活之处在于 SQL 由自己掌控,博客系统里有很多复杂的查询场景,比如按标签筛选文章、按月归档统计、搜索关键词匹配标题和内容。这些场景用 MyBatis 的动态 SQL 写起来非常舒服,比全自动 ORM 那种框框来得更直接。

当然,这套搭配并不是什么"银弹"。它的缺点也很明显,比如 MyBatis 需要手写不少 XML 映射文件,Vue 2 在 2023 年以后逐步进入维护期。但对于一个个人博客项目,这些所谓的"缺点"根本不算事——反而因为看的人多、踩过坑的人多,遇到问题的时候几乎都能搜到现成的解决方案。

2. 数据库与接口设计:开工前必须定好的两件事

2.1 博客系统到底需要几张表

很多人做博客系统,上来就建用户表、文章表、评论表、标签表、分类表……表倒是建了一堆,真到写代码的时候发现很多字段根本用不上。我个人的习惯是:先想清楚这个博客要呈现什么内容、用户能做什么操作,再来定表结构。

我的博客系统核心功能很简单:游客可以查看文章列表、查看文章详情、按标签或分类筛选、搜索、提交评论;管理员可以写文章、编辑文章、删除文章、管理评论和标签。基于这个范围,我最终设计了5张表,分别是用户表、文章表、分类表、标签表、评论表,另外加一张文章标签关联表来处理多对多关系。

用几张表的逻辑是这样的:分类和文章是一对多关系,标签和文章是多对多关系。多对多关系在关系型数据库里必须拆出一张关联表,否则你只能存逗号拼接的字符串,查询的时候非常痛苦。评论表则通过一个 parent_id 字段支持楼中楼回复,而不是单独再建一张回复表。

文章表是核心,字段设计上花了不少心思。title、content 是基本字段,但 content 我存的是富文本编辑后的 HTML,同时保留了纯文本摘要用于列表页展示。还有一个 status 字段,用来区分草稿和已发布状态。create_time、update_time 这两个时间字段看起来普通,但后面对时区踩了大坑,这个我后面会专门讲。

用户表其实可以更简单一点,因为个人博客的管理员通常就一个,我甚至见过有人直接把账号密码写死在配置文件里。但为了结构的完整性,我还是建了用户表,密码用 BCrypt 加密存储。这样以后如果想开放注册功能,不需要改表结构。

2.2 接口约定的细节决定联调效率

前后端分离项目里,接口约定是比写代码更重要的环节。前后端各写各的,如果接口的参数名、返回结构没有提前定好,联调阶段就是一场灾难。我做完这个项目最大的感受是——接口文档花一天时间好好写,能省下后面三天的扯皮时间。

我的接口返回结构统一如下:

{ "code": 200, "message": "操作成功", "data": {} }

一切接口都遵循这个格式。code 为 200 表示成功,其他为失败。data 可能是对象、数组或者 null。前端 axios 拦截器统一处理这个结构,只要 code 不是 200 就直接弹出 message 提示。这样做的好处是,前端完全不需要关心后端返回的具体格式是什么,只要按照约定取 data 就行。

具体的接口列表,我大致定了这些:

功能请求方式路径说明
文章列表GET/api/article/list分页查询,支持关键词和分类筛选
文章详情GET/api/article/{id}根据 ID 查询文章详情
发布文章POST/api/article管理员操作,需要登录
编辑文章PUT/api/article/{id}管理员操作,需要登录
删除文章DELETE/api/article/{id}管理员操作,需要登录
评论列表GET/api/comment/list按文章 ID 查询评论
提交评论POST/api/comment游客操作
标签列表GET/api/tag/list查询所有标签及文章数
登录POST/api/user/login返回 JWT token

分页参数我统一用了 pageNum 和 pageSize,从 1 开始计数。查询条件能简洁就简洁,比如文章列表的筛选,我只要一个可选的 articleTitle 关键词和 categoryId,其他一律不接收。接口设计的核心原则是:不要过度设计,前端需要什么数据,后端就给什么数据。

3. 后端实现:SpringBoot + MyBatis 的核心代码骨架

3.1 工程结构与分层思想

后端工程我用了标准的 Maven 结构,包名统一带上自己的项目标识。分层这块,我坚持的是一条简单清晰的分层链:Controller → Service → Mapper。DTO 单独建包,VO 单独建包,这样数据从数据库出来到接口返回之间,每一层传输的对象边界是明确的。

有的新手喜欢把查出来的实体对象直接返回给前端,这个习惯我觉得能改就改。数据库实体类里有很多敏感字段,比如用户表里的 password;还有创建时间、更新时间这种字段,直接暴露给前端既没必要也不安全。所以我在 Controller 层统一使用 VO 对象,只往外面暴露前端真正需要的字段。

工程结构大致如下:

src/main/java ├── controller # 接收请求 ├── service # 业务逻辑 │ └── impl # 接口实现 ├── mapper # MyBatis 的 Mapper 接口 ├── entity # 数据库实体类 ├── dto # 接收前端参数的对象 ├── vo # 返回给前端的数据对象 ├── config # 配置类(拦截器、跨域等) └── common # 通用类(返回结果、异常处理等) src/main/resources ├── mapper # MyBatis XML 映射文件 └── application.yml # 配置文件

3.2 文章接口的实现思路

文章列表接口是整个系统的基础。我要求列表只返回文章的简要信息(ID、标题、分类名、标签、摘要、发布时间),不能把全文返回,否则列表页的接口数据量会非常大。

Mapper 接口是这样的:

public interface ArticleMapper { List<ArticleVO> selectArticleList(@Param("title") String title, @Param("categoryId") Integer categoryId, @Param("offset") Integer offset, @Param("limit") Integer limit); Integer countArticle(@Param("title") String title, @Param("categoryId") Integer categoryId); ArticleDetailVO selectArticleDetail(@Param("id") Integer id); Integer insertArticle(ArticleDTO dto); Integer updateArticle(ArticleDTO dto); }

对应的 XML 映射文件里,列表查询的动态 SQL 是这么写的:

<select id="selectArticleList" resultType="com.blog.vo.ArticleVO"> SELECT a.id, a.title, c.name AS categoryName, a.summary, DATE_FORMAT(a.create_time, '%Y-%m-%d %H:%i:%s') AS createTime FROM article a LEFT JOIN category c ON a.category_id = c.id <where> <if test="title != null and title != ''"> AND a.title LIKE CONCAT('%', #{title}, '%') </if> <if test="categoryId != null"> AND a.category_id = #{categoryId} </if> </where> ORDER BY a.create_time DESC LIMIT #{offset}, #{limit} </select>

这里有几个细节值得注意。第一,用 LEFT JOIN 而不是 JOIN,是为了避免文章的分类被删掉之后导致文章不显示。第二,LIMIT 的参数是 offset 和 limit,在 Service 层里通过 pageNum 和 pageSize 计算得出:offset = (pageNum - 1) * pageSize。第三,时间格式的转换直接放在 SQL 里做,通过 DATE_FORMAT 把时间戳转成可读性更高的格式,省得在 Java 代码里再处理一遍时区。

文章详情就简单多了,直接根据 ID 查询,同时把对应的标签列表查出来。为了提高查询效率,我在 Service 层做了两步查询:文章表查基础信息,标签关联表查标签列表。短期内文章量不大,连表查询反而没有必要。

3.3 登录鉴权:JWT 的简单应用

个人博客系统的后端权限需求其实非常单薄,就管理员一个人需要登录,但我还是用 JWT 做了完整的前后端分离登录流程。原因很简单——这是前后端分离项目必须掌握的技能,博客系统只是个练手场景。

登录流程是这样的:用户提交用户名和密码,后端校验通过后,生成一个包含过期时间的 token 返回给前端。前端把 token 存在 localStorage 里,每次请求都在 Header 中携带 Authorization 字段。后端写一个拦截器,统一校验所有需要权限的接口。

JWT 的生成逻辑,我用的是 jjwt 依赖,代码大概这样:

public String generateToken(Integer userId, String username) { return Jwts.builder() .setSubject(username) .claim("userId", userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 7200 * 1000)) .signWith(secretKey) .compact(); }

签名密钥我放在了 application.yml 里,使用了比较复杂的随机字符串,同时设置了 token 过期时间为 2 小时。实际过程中前端后端的坑在时间同步上——如果服务器时间和本地时间差异过大,明明刚登录成功,下一次请求就可能提示 token 过期。所以我特意在登录接口返回 token 的同时也返回了过期时间戳,前端根据自己的判断提前做处理。

4. 前端实现:Vue 组件化与接口对接

4.1 路由与页面结构

前端项目我用 Vue CLI 创建,目录结构是标准的 src/views、src/components、src/router、src/api。页面方面,博客系统需要这么几个视图:文章列表页、文章详情页、分类归档页、标签归档页、后台管理页、登录页。

路由使用的是懒加载:

const routes = [ { path: '/', component: () => import('../views/Home.vue') }, { path: '/article/:id', component: () => import('../views/ArticleDetail.vue') }, { path: '/category/:id', component: () => import('../views/Category.vue') }, { path: '/tag/:id', component: () => import('../views/Tag.vue') }, { path: '/admin', component: () => import('../views/Admin.vue'), meta: { requiresAuth: true } }, { path: '/login', component: () => import('../views/Login.vue') } ];

meta 里的 requiresAuth 字段配合 Vue Router 的全局前置守卫,实现未登录跳转登录页。后台管理的文章编辑页和数据统计页需要的权限更高。前端做了两层的路由守卫,第一层是是否登录,第二层是是否为管理员角色。因为博客系统里管理员和普通用户的能力差距很大,虽然现在还没开放注册,但接口设计上已经留好了余地。

4.2 axios 封装和跨域处理

axios 封装几乎是每个 Vue 项目的必备步骤。我的封装思路很简单:基础配置(baseURL、超时时间)加上请求拦截器加上响应拦截器。

import axios from 'axios'; import { Message } from 'element-ui'; import router from '../router'; const service = axios.create({ baseURL: '/api', timeout: 10000 }); service.interceptors.request.use(config => { const token = localStorage.getItem('blog_token'); if (token) { config.headers['Authorization'] = token; } return config; }); service.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { Message.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); } return res.data; }, error => { Message.error(error.response?.data?.message || '网络异常'); if (error.response?.status === 401) { localStorage.removeItem('blog_token'); router.push('/login'); } return Promise.reject(error); } );

前端开发时有一个经典问题——跨域。前后端分离后,前端跑在 8080 端口,后端跑在 9090 端口,浏览器出于同源策略会拦截请求。解决方式有多种,我在开发环境用的是 Vue CLI 的代理转发,直接把 /api 开头的请求代理到后端的地址:

devServer: { proxy: { '/api': { target: 'http://localhost:9090', changeOrigin: true } } }

这样做的好处是,前端代码里不用写完整的后端地址,统一用 /api 开头。到了生产环境,再用 Nginx 做同样的转发逻辑。代码不用改一行,环境切换的成本非常低。

4.3 富文本编辑器的选型与集成

博客系统后台必须有一个像样的文章编辑器。我一开始试过自己用 textarea 加 Markdown 语法,但体验实在太差,后来直接集成了成熟的富文本方案。

我最终选择的是 wangEditor 5,原因有三个:开源免费、基于原生 JS 不依赖框架、中文文档完善。安装后,在 Vue 组件里这样初始化:

import '@wangeditor/editor/dist/css/style.css'; import { onBeforeUnmount, ref, shallowRef } from 'vue'; import { Editor, Toolbar } from '@wangeditor/editor-for-vue'; const editorRef = shallowRef(); const toolbarConfig = {}; const editorConfig = { placeholder: '请输入文章内容...' }; onBeforeUnmount(() => { if (editorRef.value) { editorRef.value.destroy(); } });

富文本编辑器有一个避不开的问题,就是内容里包含 HTML,提交给后端的时候必须考虑 XSS 安全问题。新手容易忽略这一点。我的处理方式是:后端在保存文章内容时,用一个 HtmlUtils 工具类做一次标签白名单过滤,只有 p、h1、h2、h3、img、ul、ol、li、blockquote 等常用标签才被保留,其余一律剥离。这个过滤逻辑虽然简单,但能拦掉绝大多数的存储型 XSS 攻击。

5. 本地跑通的完整流程:从 clone 到 F5

5.1 环境准备清单

我不太喜欢那种"照着敲一遍代码就能跑"的教程,因为环境问题本身就是项目能否跑通的关键变量。先列一个我认为必须具备的环境清单:

  • JDK:1.8 及以上,建议 11。我的项目编译级别是 1.8,换更高的版本一般也没有问题。
  • Maven:3.6 及以上,用来管理后端依赖。
  • Node.js:14.18 及以上,低于这个版本 Vue CLI 的项目可能会报警告。
  • MySQL:5.7 及以上,我用的是 5.7,一个经典的稳定版本。
  • IDE:后端用 IntelliJ IDEA,前端用 VS Code,这个全凭个人喜好。

初次把项目拉下来,我强烈建议先花半小时把这些环境版本对齐,尤其是 Maven 和 Node 的版本,差一个大的版本号就可能出现装不上依赖的情况。

5.2 一步步启动后端

第一步是准备数据库。我的工程目录下放了一个 init.sql 文件,里面包含了建库、建表、插入基础数据的完整 SQL。用 Navicat 或者命令行执行都可以。

CREATE DATABASE IF NOT EXISTS blog_system DEFAULT CHARACTER SET utf8mb4; USE blog_system; CREATE TABLE article ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT '文章ID', title VARCHAR(200) NOT NULL COMMENT '标题', content MEDIUMTEXT COMMENT '内容', summary VARCHAR(500) COMMENT '摘要', category_id INT DEFAULT NULL COMMENT '分类ID', status TINYINT DEFAULT 1 COMMENT '0草稿 1已发布', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里我特意强调一下 charset,建议使用 utf8mb4,而不是 utf8。因为 utf8 在 MySQL 里只能存储最大 3 字节的字符,遇到部分 emoji 和生僻字就直接报错了。博客内容是重灾区。

第二步是修改 application.yml 里的数据库连接信息、端口号,以及自定义的 JWT 配置。第三步,用 IDEA 打开后端工程,等待 Maven 下载依赖结束后,直接运行主类。启动日志出现"Tomcat started on port(s): 9090"就说明后端成功了。

5.3 一步步启动前端

前端启动就更简单了。先把工程目录下的 package.json 里的依赖装好,执行 npm install 安装依赖。如果下载慢,可以换成国内的镜像源。

然后执行 npm run serve,默认会启动在 8080 端口。注意 Vue CLI 的 devServer 代理配置我已经放在 vue.config.js 里了。如果后端跑在 9090 端口,前端访问 /api 就会自动代理过去。打开浏览器输入 http://localhost:8080 就能看到博客首页。

本地跑通整个项目,最大的一个心理预期是:第一次跑通往往不会一帆风顺,大多数问题都出在环境差异而不是代码本身。我的建议是,如果遇到报错,先看控制台的完整堆栈信息,不要只看最下面一行。比如数据库连接失败,后端日志一定会明确提示是连接超时、账号密码错误还是数据库不存在。

6. 部署上线:服务器上跑起来的每一步

6.1 打包后端:可执行 jar 的坑

前后端分离项目部署时,后端打包的过程有一个非常典型的坑。Maven 默认的打包方式,打出来的 jar 包是可运行 jar,但前提是 pom.xml 里引入了 spring-boot-maven-plugin。如果没有这个插件,打出来的 jar 包执行 java -jar 时会报"没有主清单属性"。

正确的插件配置如下:

<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>

配置好之后,在项目根目录执行 mvn clean package -DskipTests,target 目录下就会生成两个 jar 包。一个是 xxx.jar,另一个是 xxx.jar.original。我们需要的是前者,它是一个自包含的可执行包,内嵌了 Tomcat 和所有依赖。

服务器上部署,我用的是最稳妥的方式:把 jar 包传到服务器,用 nohup 命令启动:

nohup java -jar blog-backend.jar \ --spring.profiles.active=prod \ > /var/log/blog/backend.log 2>&1 &

这里我觉得有必要补充说明一下。spring.profiles.active=prod 这个参数意味着我准备了多套配置,application-prod.yml 的内容是生产环境的数据库地址、端口等。开发环境和生产环境的配置通过 profile 机制区隔,这样就不需要每次部署都改配置文件了。

6.2 前端打包与 Nginx 配置

前端的部署比后端多一层——静态资源需要一个 Web 服务器。我最常用的是 Nginx,配好之后可以同时解决静态文件服务和 API 反向代理这两个核心问题。

在前端项目目录执行 npm run build,生成的 dist 目录就是打包后的静态文件。把 dist 目录上传到服务器,比如 /var/www/blog/ 下。然后在 Nginx 的配置里写一个 server 块:

server { listen 80; server_name your_domain.com; # 前端静态资源 root /var/www/blog; index index.html; # 单页应用路由重写 location / { try_files $uri $uri/ /index.html; } # API 反向代理 location /api/ { proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ { expires 7d; add_header Cache-Control "public, no-transform"; } }

Nginx 配置里最容易踩的坑是 try_files 这一行。Vue Router 用的 history 模式,如果没有这行,用户在浏览器直接访问 /article/3 时会返回 404,因为 Nginx 找不到这个路径对应的真实文件。加上 try_files $uri $uri/ /index.html 之后,前端路由交给 Vue Router 处理,页面就能正常打开了。

API 代理这里,location /api/ 下的 proxy_pass 注意结尾要带 /。不带斜杠和带斜杠的区别,真的会让不少人头疼。简单理解就是:带斜杠表示把 /api/ 前缀替换掉,不带斜杠表示保留原路径。我这里后端接口本身的路径就带 /api,所以 proxy_pass 不带斜杠是正确姿势。

6.3 数据库迁移与备份

服务器上的 MySQL 数据库,我用了另一种实用做法:本地数据库先执行 mysqldump 导出,再在服务器上导入。

mysqldump -u root -p blog_system > blog_backup.sql scp blog_backup.sql root@server_ip:/home/ mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS blog_system DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p blog_system < /home/blog_backup.sql

数据备份这件事,我觉得是个人项目里最容易偷懒但绝对不能少的一环。博客系统里你写了几百篇文章,服务器一坏全没了,那种损失是任何教程都无法弥补的。我给服务器配置了一个简单的定时任务,每天凌晨自动备份数据库并保留最近 7 天的备份文件:

0 2 * * * mysqldump -u root -p密码 blog_system | gzip > /home/backup/blog_$(date +\%Y\%m\%d).sql.gz

如果预算允许,还可以把备份同步到对象存储服务。一个两分钟能搭好的定时任务,换来的是一整块安心。

7. 我在这个项目里踩过的坑

7.1 时间字段的时区问题

这个坑我印象太深了。开发的时候,前端页面上发布文章时间显示正常,部署到服务器之后,发现时间比实际时间慢了 8 个小时。问题出在 MySQL 连接串上。服务器和数据库的时区不一致,MySQL 默认使用系统时区,而 JDBC 连接串里又没有明确指定 serverTimezone,导致时间读取出来是 UTC 时间,比北京时间少 8 小时。

解决方式是在 JDBC 连接串里加上 serverTimezone=Asia/Shanghai:

spring: datasource: url: jdbc:mysql://localhost:3306/blog_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

另外一个相关的细节是数据库连接串里的 characterEncoding=utf8,在高版本 MySQL 驱动下,实际会映射为 utf8mb4。但为了保险,建库的时候就要把字符集定成 utf8mb4,否则还是可能出现中文乱码。

7.2 MyBatis 映射字段名对不上的经典场景

MyBatis 有一个新手必踩的坑:数据库字段名是下划线风格(如 create_time),而 Java 实体类字段名是驼峰风格(如 createTime)。如果 xml 里没有配置自动映射,查询结果里的 createTime 就是 null。

解决方案有两种。第一种是显式在 xml 里用 resultMap 做字段映射,第二种是打开 MyBatis 的驼峰命名自动映射开关:

mybatis: configuration: map-underscore-to-camel-case: true

我在项目里开了自动映射之后,大部分字段都没问题了,但有些特殊场景还是需要 resultMap。比如多表联查返回 VO 对象时,子查询的字段没法通过简单的驼峰映射解决,老老实实写 resultMap 反而更清晰。

7.3 跨域的坑不只在后端

跨域问题,很多人以为后端配置一个 CorsFilter 就万事大吉了。实际联调的时候我觉得跨域的坑远不止这一个。比如我开发时用 Vue CLI 代理,生产时用 Nginx 反向代理,前后端的域名如果不同,就不是简单的 CorsFilter 能解决的。

强调一个核心思路:能用反向代理解决跨域,就尽量不要在代码层面放开跨域。因为一旦放开跨域,等于你的后端接口可以被任意域名调用,安全风险会明显增加。Nginx 反向代理模式下,浏览器看到的请求是同源的——都是你的服务器域名,浏览器不会产生跨域拦截。这种情况加 CorsFilter 反而多此一举。

把这一行配置去掉之后,我再也没有遇到过 OPTIONS 预检请求的纠缠。遇到过不少朋友问我,为什么明明配了 CorsFilter 还是报跨域错误——一看配置,Nginx 代理过去时把请求头里的 Origin 字段带上了,后端放开的是固定域名,结果被拦截掉了。这种情况,直接把跨域放开改为代理解决就好。

7.4 富文本编辑器图片上传的两个方案

富文本编辑器上传图片,我前后换了两种方案。最初我打算把图片转成 base64 存储在数据库中,结果写了几篇文章之后数据库体积明显增大,文章列表接口也变慢。后来改成图片上传接口,前端把图片传到服务器指定目录,数据库只存储图片的 URL 路径,两边都轻松很多。

图片上传接口我用了一个简单的 MultipartFile 处理:

@PostMapping("/api/upload") public ResultVO upload(@RequestParam("file") MultipartFile file) { String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); String filename = UUID.randomUUID().toString().replace("-", "") + suffix; String monthDir = "upload/" + LocalDate.now().toString(); File dir = new File(uploadPath, monthDir); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir.getAbsolutePath(), filename)); String url = "/" + monthDir + "/" + filename; return ResultVO.success(url); }

文件名用 UUID 是必须的,否则不同用户上传同名图片会互相覆盖。按日期按月建目录,这样后期清理过期图片也方便。服务器上的 Nginx 再配一个 upload 目录的映射,图片就能通过 URL 直接访问了。

8. 一些建议:这个项目做完之后我最大的体会

整个项目从设计到落地,前后用了大概两周业余时间。做这个博客系统的过程中,我最明显的感觉是:前后端分离的方案,让个人博客项目的维护变得轻松很多。比如我想换个前端主题,完全不影响后端代码,重新写 Vue 页面打包部署即可。后端想加一个统计接口,前端不用做任何改动,新接口发布后直接调用就行。

对新手来说,这个项目确实是练手的好选择。它不像企业级项目那样有复杂的业务逻辑,但麻雀虽小五脏俱全——数据库设计、CRUD、登录鉴权、文件上传、跨域处理、打包部署,这些前后端分离开发中的核心环节全都覆盖到了。完全照着完整的源码和部署教程走一遍,再独立从头做一遍,前后端分离的基本功就扎实了。

最后分享一个我做这个小项目时学到的经验。代码写不出来的时候,不要硬憋。先把接口文档写清楚,把表结构设计好,把项目能跑通作为第一阶段目标。代码的循环和节点可以逐步完善,但项目从 0 到 1 跑通这个进程,带来的信心比什么都重要。希望你的第一个前后端分离博客项目,也能从头到尾走完,遇到问题的时候回来再看看这篇文章里提到的那些坑,至少能少走几步弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 3:07:53

LogViewer:终端原生日志探针,秒级定位OOM与超时错误

简介&#xff1a;这是一款面向开发、运维及测试人员的轻量级日志实时查看与分析工具——LogViewer Pro&#xff0c;专为高效处理超大体积日志文件设计&#xff0c;实测可稳定加载并流畅浏览46GB级别日志&#xff0c;显著缓解传统文本编辑器卡顿、崩溃等痛点。资源包为551KB的ZI…

作者头像 李华
网站建设 2026/10/17 3:07:21

智能牧场AI监控产犊全流程:从算法到部署的实战解析

凌晨两点半&#xff0c;值班手机突然震动&#xff0c;屏幕上跳出一条告警&#xff1a;3号牛舍B12栏位母牛检测到持续努责行为&#xff0c;胎衣已破&#xff0c;预估30分钟内产出。在以往&#xff0c;这个时间点我可能已经顶着寒风跑了两趟牛舍&#xff0c;襁褓里的犊牛能不能活…

作者头像 李华
网站建设 2026/10/17 3:07:21

Linux多命令合并执行全解:分号、管道、函数,一篇讲透

说实话&#xff0c;我刚接触“Linux将多条指令合并为一条执行”这个玩法时&#xff0c;是有点不以为然的。当时在服务器上维护一套静态资源&#xff0c;每天重复做三件事&#xff1a;备份、压缩、清理。每次都要先敲备份命令&#xff0c;等它跑完再敲压缩&#xff0c;最后再删旧…

作者头像 李华
网站建设 2026/10/17 3:06:35

从反馈真空到AI写作教练:一款英语写作APP的技术实现与成本优化

很多人学英语卡住的地方&#xff0c;不是词汇量不够&#xff0c;而是写完之后“没人管”。上大学那会儿我写英文作文&#xff0c;交上去要等老师好几天才能返回来&#xff0c;批注还多半是几句“注意时态”“表达不地道”这类笼统的话。工作以后写英文邮件&#xff0c;自己读三…

作者头像 李华
网站建设 2026/10/17 3:06:14

双串锂电池主动均衡设计:MP2642GR-Z与ATmega644协同方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 3:06:10

智能组卷与错题攻坚:考试通关系统的核心设计

很多同学复习考试&#xff0c;最常做的事就是“打开题库&#xff0c;拼命刷题”。刷多了你会发现一个奇怪的现象&#xff1a;做了几千道题&#xff0c;正确率没怎么涨&#xff0c;错过的题换个数字换个说法&#xff0c;照样掉坑。这不是努力的问题&#xff0c;是复习系统本身缺…

作者头像 李华