简介:本资源是一套完整的校园博客系统课程设计与毕业设计参考实现,面向计算机相关专业本科生及Java全栈初学者,解决学生/教师在校园场景下便捷发布、互动与管理技术博客的实际需求。压缩包共794个文件,18.32MB,涵盖106个Java后端业务与配置类(SpringBoot核心逻辑)、42个Vue组件(含登录、博客列表、发布、评论、点赞等交互界面)、153个JS脚本(前端路由与状态管理)、65个JPG/PNG图片及79个GIF动效资源,辅以SQL建表语句、YML配置、BAT/CMD部署脚本及HTML模板等,结构清晰、开箱即用。已有233人学习下载,适合快速掌握前后端分离开发流程。读者可直接运行三步脚本(install→run→build)启动系统,获得含权限分级(学生/教师角色)、完整RESTful API、响应式UI及典型CRUD+点赞评论交互的可演示项目,是理解Vue+SpringBoot工程化实践的优质学习案例。
从头撸一套校园博客系统:SpringBoot + Vue 从设计到部署的完整复盘
毕业设计、课程项目、社团官网、实验室博客……“校园博客系统”这个题目,我在很多地方都见过。但说实话,市面上能直接跑起来、代码又干净、还能让人看懂的设计方案,真不多。大部分要么是纯 Demo 级别,要么为了演示功能过度堆砌。这次我把整条链路——从需求分析、技术选型、数据库设计、前后端编码,到最后的 Linux 部署,完整过了一遍,输出一个可以直接拿去用的校园博客系统。这篇就把核心设计思路、关键代码逻辑和部署过程中踩过的坑都摊开来讲。
这套系统基于 SpringBoot + Vue 全栈实现,围绕“校园”这个场景做了一系列针对性设计,比如学院/专业维度的文章分类、学生/教师双角色权限体系、辅导员审核机制等。源码、部署说明、系统介绍文档都整理在包里了,适合正在做毕设、想系统学习全栈项目、或者单纯需要一个可扩展脚手架的人。无论你是第一次接触 SpringBoot 的 Vue 新手,还是想快速搭一套内容管理系统的老手,这篇复盘都能让你少走不少弯路。
1. 项目定位:校园博客不只是一个“博客”
很多人拿到“校园博客系统”这个题目,第一反应就是把普通的个人博客加个注册登录就交差了。真这么做,答辩或展示的时候很容易被追问到哑口无言。校园博客系统的核心差异不在“博客”两个字,而在“校园”两个字。
1.1 场景化需求拆解
我最初梳理需求时,先画了一个非常朴素的问题清单:
- 谁在使用这个系统?——在校学生、教师、系统管理员,这是三个完全不同的角色。
- 他们在什么场景下使用?——学生写学习笔记、社团活动总结、技术分享;教师发通知、发课程资料;管理员做内容监管、用户管理。
- 校园场景下有什么特殊要求?——内容不能完全失控(所以需要审核机制)、信息要按院系组织(所以需要组织架构维度)、用户身份要能识别(所以学生和教师的权限有差异)。
基于这些问题,我敲定了三个核心设计目标:
- 统一的身份认证和角色权限体系:登录后根据角色动态渲染菜单和操作按钮,而不是前端把页面写死。
- 院系维度驱动的内容组织:文章、分类、用户都与学院/专业关联,支持按院系筛选内容,这是校园场景区别于通用博客的重要特征。
- 内容审核链路:学生发布的文章默认进入待审核状态,由辅导员或管理员审核后上线。这是真实校园环境的刚需,也是这个项目区别于普通个人博客的一大亮点。
1.2 功能模块边界划分
系统拆成六大模块,每个模块的职责边界从一开始就划清楚:
| 模块 | 核心功能 | 角色权限 |
|---|---|---|
| 用户模块 | 注册、登录、个人信息维护、密码修改 | 游客可注册,登录后可用 |
| 文章模块 | 发布、编辑、删除、草稿、审核、置顶 | 学生/教师可发布,管理员审核 |
| 分类模块 | 按院系维护分类树 | 管理员维护 |
| 评论模块 | 文章评论、回复、删除 | 登录用户可评论,作者/管理员可删 |
| 通知模块 | 审核结果通知、系统公告 | 系统自动生成,管理员可发公告 |
| 统计模块 | 文章浏览量、用户数、分类文章数 | 管理员可见 |
边界明确之后,前后端的接口设计就顺了。每个模块内部再拆 Controller、Service、Mapper 三层,代码结构一目了然,后续加需求也不至于牵一发动全身。
2. 技术选型:为什么是 SpringBoot + Vue,而不是别的
这套组合现在几乎是全栈项目的默认答案,但“默认答案”不等于“没得选”。我做选型时重点权衡过三个问题。
2.1 后端选型的核心考量
SpringBoot 的优势在于约定大于配置,内置 Tomcat,少了大量 XML 配置,尤其适合快速交付。我用的版本是 SpringBoot 2.7.x,配合 JDK 1.8,稳定性和生态兼容性都是经过大量生产环境验证的。
ORM 层面我选了 MyBatis-Plus 而不是纯 MyBatis。原因很朴素:
- 单表 CRUD 不用手写 SQL,代码量直接砍半;
- 内置分页插件,列表接口几行代码搞定;
- 字段自动填充(create_time、update_time)和逻辑删除是开箱即用的。
学习成本上,如果你熟悉 MyBatis 的 XML 写法,MyBatis-Plus 几乎零成本上手;如果你完全不了解,它也足够简单,文档很清晰。
2.2 前端框架的适配性分析
Vue 2 还是 Vue 3?我当时权衡了很久。Vue 3 的组合式 API 确实更现代,但考虑到毕设/课设场景中最常见的诉求是“快速跑通 + 教程多 + 遇到问题能搜到答案”,我最终选了 Vue 2 + Element UI。原因很现实:Vue 2 的中文资料量是 Vue 3 的几倍,Element UI 的表格、表单、弹窗组件能覆盖后台管理 90% 的需求。
前端工程化用的是 Vue CLI 4.x。Webpack 构建虽然比 Vite 慢,但胜在生态成熟,很多老项目模板都是基于它,遇到奇怪的构建问题更容易找到解决方案。
2.3 其他关键依赖
| 技术 | 选型 | 理由 |
|---|---|---|
| 数据库 | MySQL 5.7+ | 稳定、普及率高、资料多 |
| 认证方案 | JWT + 拦截器 | 前后端分离项目标配,无状态、易扩展 |
| 接口文档 | Swagger/Knife4j | 调试方便,演示时直接打开文档页加分 |
| 文件存储 | 本地目录 + Nginx 映射 | 简化部署,不做分布式存储,够用且好懂 |
这套选型组合的另一个好处是“面试友好”。每一个选型都能展开讲出理由,而不是“大家都用所以我也用”。后面部署和排错时你会发现,这套组合的坑大多有人踩过,基本都能找到对应解法。
3. 数据库设计:核心表结构和关键决策
数据库设计是整系统的地基,地基不稳后面全要返工。这个项目的库名我定义为campus_blog,编码 utf8mb4,排序规则 utf8mb4_general_ci。utf8mb4 必须强调,不是 utf8——因为评论和文章里可能有人发 Emoji,utf8 存不下四字节字符,会直接报错。
3.1 核心表结构
用户表user是权限体系的基石:
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(100) NOT NULL COMMENT '加密后的密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `avatar` varchar(255) DEFAULT NULL COMMENT '头像地址', `role` tinyint(4) NOT NULL DEFAULT '1' COMMENT '角色: 0管理员 1学生 2教师', `college_id` bigint(20) DEFAULT NULL COMMENT '所属学院', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态: 1正常 0禁用', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';密码字段这里有一点必须提醒:不要存明文。我用的加密方式是 BCrypt,Spring Security 里的BCryptPasswordEncoder,虽然这个项目没有整体引入 Spring Security(避免过度设计),但单独引入spring-security-crypto依赖做密码加密和校验还是很有必要的。
文章表article是内容核心:
CREATE TABLE `article` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '作者ID', `title` varchar(200) NOT NULL COMMENT '标题', `summary` varchar(500) DEFAULT NULL COMMENT '摘要', `content` longtext COMMENT '正文', `cover` varchar(255) DEFAULT NULL COMMENT '封面图', `category_id` bigint(20) DEFAULT NULL COMMENT '分类ID', `view_count` int(11) NOT NULL DEFAULT '0' COMMENT '浏览量', `like_count` int(11) NOT NULL DEFAULT '0' COMMENT '点赞数', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态: 0草稿 1待审核 2已发布 3已驳回', `audit_reason` varchar(255) DEFAULT NULL COMMENT '审核驳回原因', `is_top` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否置顶: 1是 0否', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_category_id` (`category_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章表';status字段是文章模块的关键设计。用 0/1/2/3 四个数字表示草稿、待审核、已发布、已驳回四个状态,每个状态之间的流转必须有明确的接口约束。比如:草稿只能自己看,待审核状态作者不能重复提交,已驳回必须填写原因。
学院表college和分类表category是校园维度的支撑。学院表很简单但很重要,文章和用户都依赖它;分类表我做了两级结构——一级是学院,二级是该学院下的分类。这样做的好处是:分类天然隔离,计算机学院看不到外国语学院的分类,符合校园场景的组织逻辑。
3.2 索引设计的取舍
索引不是越多越好。我一开始给article表加了不少索引,后来发现有些索引压根不会被查询用到,反而拖慢了插入速度。最终保留了三个最核心的索引:
idx_user_id:查询“某人发布的文章”必用;idx_category_id:按分类筛选文章必用;idx_status:列表页默认查询已发布文章(status=2)必用。
联合索引我特意没有多建。一张表如果只有一个查询场景是同时过滤多个条件,建联合索引收益不大。比如列表页可能是“某个分类 + 已发布 + 按时间排序”,这种场景下单独给category_id和status建索引已经足够,MySQL 会自动用索引合并或者交集筛选,实际性能测试下来 10 万条数据内的查询都在毫秒级。
4. 后端核心实现:从接口设计到权限控制
后端用 SpringBoot 做,包结构是标准的controller/service/serviceImpl/mapper/entity/dto/vo/config/common。这块的重点不在代码量,而在几个关键设计决策。
4.1 接口统一返回格式
前后端分离项目,接口返回格式如果不统一,前端每个请求都要单独处理异常情况,代码会臭不可闻。我定义了一个通用的Result<T>类:
@Data public class Result<T> implements Serializable { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }同时搭配一个全局异常处理器,用@RestControllerAdvice捕获所有业务异常和系统异常,保证接口层永远只返回统一结构,不会把堆栈信息直接甩给前端。前端拿到code != 200时统一弹出 message,开发效率提升明显。
4.2 JWT 认证与拦截器
短信、Session、OAuth 这些方案中我选了 JWT 做认证。核心逻辑如下:
- 用户登录成功后,后端用用户的 id 和角色生成一个 token,有效期设置 7 天;
- 前端把 token 存在 localStorage,每次请求在 axios 拦截器里加到
Authorization头; - 后端写一个
JwtInterceptor,实现HandlerInterceptor接口,在preHandle里校验 token 并从 token 解析出用户信息放到 ThreadLocal 中; - 注册拦截器时用
addPathPatterns指定拦截路径,excludePathPatterns放行登录、注册、文章详情等无需认证的接口。
这里有个坑值得提醒:拦截器放行接口列表要谨慎。我最初把文章浏览量 +1 的接口也放行了,结果被脚本刷了几万次浏览量。后来把这个接口改成在查询文章详情时顺便 +1,并且加上 5 分钟的 Redis 缓存控制(或者简单的内存记录也行),虽然有并发下计数不精确的问题,但展示场景完全够用,还能防刷。
4.3 文章审核状态机
文章从创建到发布的流转,我设计成一个简单的状态机:
草稿(0) -> 待审核(1) -> 已发布(2) ^ | | v +--- 已驳回(3)对应的接口逻辑是:
- 保存草稿接口:状态置为 0,只有作者自己和管理员可见;
- 提交审核接口:状态从 0 或 3 变为 1,需要校验当前用户是作者本人,且文章有标题和正文;
- 审核通过接口:状态从 1 变为 2,只有管理员(或辅导员角色)能操作;
- 审核驳回接口:状态从 1 变为 3,必须填写
audit_reason,驳回原因会通过通知模块推送给作者。
这个状态机在 Service 层用 if-else 判断,逻辑不算复杂,但每个状态转移都验证了“当前状态是否符合预期”,比如禁止把已发布的文章改回草稿(除非先下架),避免数据错乱。
4.4 多条件分页查询的 SQL 写法
文章列表页需要支持按分类、按状态、按关键词搜索,还要分页。MyBatis-Plus 的分页插件配置好之后,用 LambdaQueryWrapper 就非常舒服:
public IPage<ArticleVO> pageArticles(ArticleQuery query) { Page<Article> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>(); // 按分类过滤 if (query.getCategoryId() != null) { wrapper.eq(Article::getCategoryId, query.getCategoryId()); } // 按状态过滤 if (query.getStatus() != null) { wrapper.eq(Article::getStatus, query.getStatus()); } // 按标题模糊搜索 if (StringUtils.hasText(query.getKeyword())) { wrapper.like(Article::getTitle, query.getKeyword()); } // 置顶文章排前面,新的排前面 wrapper.orderByDesc(Article::getIsTop) .orderByDesc(Article::getCreateTime); return articleMapper.selectPage(page, wrapper); }注意orderByDesc(Article::getIsTop)这里要把置顶文章排在最前面。校园博客场景下,院系重要通知需要置顶展示,这个排序逻辑几乎是刚需。
5. 前端页面架构:Vue 侧的路由、状态管理和组件复用
前端这块我用 Vue 2 + Element UI 搭了一套后台管理界面。页面分为三个层级:游客可访问的博客前台(文章列表、详情、评论),登录用户的工作台(写文章、管理自己的文章),管理员的控制台(用户管理、审核、分类管理、统计)。
5.1 路由设计中的权限控制
路由分三类配置:
- 公开路由:首页文章列表、文章详情、登录页、注册页,任何人可访问;
- 用户路由:写文章、我的文章、个人中心,需要登录;
- 管理路由:用户管理、审核管理、分类管理、数据统计,需要管理员身份。
前端在main.js里注册全局前置守卫:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.meta.requiresAuth && !token) { next('/login'); return; } if (to.meta.requiresAdmin) { const role = localStorage.getItem('role'); if (role !== '0') { next('/'); return; } } next(); });这是第一道防线。第二道防线在后端,管理员接口加了@RequireRole(role = "0")的注解供拦截器校验。前端控制页面显示,后端口令必须校验角色,双保险。
5.2 axios 封装与 token 刷新
axios 实例统一封装,核心逻辑是请求拦截器里加 token,响应拦截器里统一处理错误码:
service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = token; } return config; }); service.interceptors.response.use( response => { const res = response.data; if (res.code === 200) { return res; } if (res.code === 401) { localStorage.clear(); router.push('/login'); } return Promise.reject(new Error(res.message)); }, error => { Message.error(error.message || '请求失败'); return Promise.reject(error); } );这个封装的收益是每个页面的业务代码里几乎看不到重复的错误处理逻辑,代码干净很多。
5.3 富文本编辑器的集成细节
写文章需要富文本编辑器,我集成的是wangEditor5.x 版本,轻量、中文文档齐全、配置简单。核心集成步骤:
- 安装
@wangeditor/editor和@wangeditor/editor-for-vue; - 在组件里创建 editor 实例,配置上传图片的 server 地址;
- 图片上传走自己封装的文件上传接口,返回的是 Nginx 映射后的 URL。
集成过程中遇到一个比较典型的问题:富文本编辑器的联动状态。使用富文本编辑器时,需要监听变化后实时把 HTML 内容同步到 Vue data 里,否则表单提交时拿到的是旧数据。
const handleChange = (editor) => { form.content = editor.getHtml(); };另外,图片上传后默认返回<img src="...">是正常的,但要注意 URL 拼接逻辑。我统一约定后端返回相对路径(如/upload/2024/03/xxx.jpg),前端展示时加上后端域名前缀。这样做的目的是防止服务器 IP 或端口变化导致图片链接全部失效。
5.4 Element UI 表格和分页的坑
管理后台的表格 + 分页组合,Element UI 的el-pagination组件需要特别注意几个属性的绑定逻辑:
<el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="query.pageNum" :page-sizes="[10, 20, 50]" :page-size="query.pageSize" layout="total, sizes, prev, pager, next, jumper" :total="total"> </el-pagination>翻页时,除了更新pageNum,必须重新请求列表数据。很多新手容易忘记把query.pageNum传回后端,导致点击第二页但还是显示第一页的数据。另外,切换每页条数时要把pageNum重置为 1,否则可能在只有 2 条数据的第三页上看不到内容。
6. 部署篇:从本地到 Linux 服务器的完整流程
开发环境跑通了只是第一步,真正能交付的东西必须能部署到服务器上。我用的服务器是 2 核 4G 的云主机,系统 CentOS 7.9,部署方案:Nginx 托管前端静态文件 + 反向代理后端接口,后端 Jar 包用 systemd 守护进程托管。
6.1 后端打包和启动
SpringBoot 应用用 Maven 打包:
mvn clean package -DskipTests打包产物是target/campus-blog.jar。上传到服务器后,我建了一个 systemd 服务文件/etc/systemd/system/campus-blog.service:
[Unit] Description=Campus Blog Application After=network.target [Service] Type=simple User=root WorkingDirectory=/opt/campus-blog ExecStart=/usr/bin/java -Xms512m -Xmx1024m -jar /opt/campus-blog/campus-blog.jar --spring.profiles.active=prod Restart=on-failure RestartSec=10 SuccessExitStatus=143 [Install] WantedBy=multi-user.target注意几个细节:
Restart=on-failure保证进程挂掉后 10 秒自动拉起;SuccessExitStatus=143是 Java 应用被 systemd stop 时正常退出的返回码,不加的话 stop 会被判定为失败;- JVM 参数
-Xms512m -Xmx1024m是给 2C4G 机器留了余量,避免 OOM。
启动命令:
systemctl daemon-reload systemctl enable campus-blog systemctl start campus-blog6.2 Nginx 配置
Nginx 同时负责前端静态文件服务和 API 反向代理:
server { listen 80; server_name your-domain.com; # 前端静态文件 root /opt/campus-blog/dist; index index.html; # 前端 history 路由刷新时转发到 index.html location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1: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; } # 上传文件映射 location /upload/ { alias /opt/campus-blog/upload/; } }这里最经典的坑是try_files $uri $uri/ /index.html;这一行。如果不加,Vue Router 用 history 模式时,刷新非首页的 URL(比如/article/12)会直接 404。加了这个配置后,Nginx 会把所有不存在的路径都转发到 index.html,由前端路由接管。
6.3 跨域问题的正确解法
开发环境的前端跑在localhost:8080,后端跑在localhost:9090,跨域是必然的。我同时在两层做了处理:
- 后端加了一个 CORS 配置类,允许跨域请求;
- 开发环境用 Vue CLI 的 devServer 代理把
/api转发到后端。
部署到生产环境后,前端和后端在同一个 Nginx 域名下,不存在跨域问题。但有个细节要注意:后端的 CORS 配置在生产环境建议限制允许的源,不要用allowedOriginPatterns("*")这种全放开的写法,安全合规角度考虑,能关就关。
6.4 MySQL 导入和连接配置
项目包里带的sql/campus_blog.sql是完整的建库建表 + 初始数据脚本。部署时执行:
mysql -uroot -p < campus_blog.sql然后修改application-prod.yml中的数据库连接配置:
spring: datasource: url: jdbc:mysql://localhost:3306/campus_blog?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai username: your_username password: your_passwordJDBC URL 里的characterEncoding=utf8mb4是关键,漏掉这个参数,即使表结构是 utf8mb4,写入的中文也可能乱码。serverTimezone=Asia/Shanghai不能省略,MySQL 8 默认时区跟本地差 8 小时,不加的话时间字段全部错位。
7. 实战踩坑记录:这些问题花了我最多时间
最后这部分记录几个我实际调试过程中花时间最多的坑,权当给后来者一个避雷参考。
7.1 SpringBoot 版本太新导致的依赖冲突
项目一开始我用的是 SpringBoot 3.x,结果 MyBatis-Plus、Knife4j 这些依赖还没完全适配,各种类找不到、配置不生效的问题层出不穷。后来老老实实换回 SpringBoot 2.7.x,所有依赖一键拉齐,整个世界清净了。
教训是:技术上赶新不赶旧,但在实际项目中要选生态匹配度最高的版本组合。你在网上搜到的大部分博客教程,讲的都是 SpringBoot 2.x,遇到问题容易找到答案。用 3.x 出了奇怪的问题,可能搜半天都搜不到解决方案。
7.2 Vue 项目 npm install 一直失败
npm install 装到一半报各种网络错误。换过淘宝镜像还是一样。最后排查发现是 Node 版本太高(Node 18),导致 node-sass 编译失败。解决方案是换成sass(dart-sass),并且把 Node 版本降到 16。
现在新项目我默认用sass而不是node-sass,后者已经停止维护很久了,兼容性坑太多。
7.3 富文本图片上传的浏览器缓存问题
图片上传成功后,如果修改图片再上传同名文件,浏览器会命中缓存导致显示旧图片。解决办法是上传时在 URL 后面加时间戳参数:
// 上传成功后拼接 URL const url = res.data.url + '?t=' + Date.now();这不算大问题,但在演示的时候特别容易翻车——改了个图片发现页面没变,会非常尴尬。
7.4 部署后接口报 404 的一个隐蔽原因
有次部署后所有/api/请求全部 404,前端控制台报错。一开始以为是 Nginx 的问题,排查半天发现是后端接口路径写错了——@RequestMapping注解的值少了个/,导致实际路径是/apiarticle/list而不是/api/article/list。这种低级错误在本地联调时因为前端代理配置恰好匹配,可能不会暴露,但部署到生产环境后路径就对不上了。
排查方法其实很简单:打开浏览器的 Network 面板看具体请求的 URL,对比接口文档,一眼就能看出来。但很多人在这时候会先去检查防火墙、检查 Nginx 配置,反而忽略了最基础的路径问题。
7.5 Linux 服务器端口无法访问
服务器上部署好后,本机访问http://服务器IP:8080一直超时。排查链路:
- 先确认进程在跑:
ps aux | grep campus-blog; - 再确认端口监听了:
netstat -tlnp | grep 8080; - 最后才发现云服务商的安全组规则里没有放行 8080 端口。
云服务器除了要配置操作系统防火墙(firewalld/iptables),还要在云控制台的安全组里放行对应端口。这个坑是云环境特有的,本地虚拟机永远不会遇到。如果你在外网访问不了服务,先别慌,按这个顺序排查基本能定位。
写在最后的几个建议
代码本身能跑只是及格线,能讲清楚设计思路才是加分项。如果你要拿这个项目做毕设答辩或者面试展示,建议多花点时间想清楚这几个问题:为什么用 JWT 不用 Session?为什么文章状态要分四层?为什么前端要封装 axios?这些在本文里其实都隐含了答案。
部署上的细节,比如 systemd 的SuccessExitStatus=143、Nginx 的try_files、MySQL 的characterEncoding=utf8mb4,这些是网上很多教程不会特意讲的,但恰恰是实际部署最容易卡住的地方。如果遇到问题,优先用这种“从现象反推链路”的思路去排查,比盲目搜报错关键词更高效。
本文还有配套的精品资源,点击获取