拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

在线教育系统微服务架构拆解:SpringCloud+Vue项目实战解析

在线教育系统微服务架构拆解:SpringCloud+Vue项目实战解析

简介:在线教育系统(online_edu)项目包,面向Java后端开发学习者与微服务架构实践者,提供一套完整的前后台分离教学案例。系统涵盖课程、问答、文章三大前台模块,后端采用SpringBoot + SpringCloud + MyBatis-Plus整合方案,结合MySQL分库设计、Redis缓存、ActiveMQ消息队列,并接入阿里云OSS与视频点播服务;前端基于Node.js + Vue.js构建。业务层还包含JWT单点登录、Swagger接口文档、POI批量导入用户、ECharts图表展示等实用功能,可作为毕业设计或简历项目参考。压缩包为zip格式,大小约198KB,目前已有1154人学习下载。资源内含项目源码与配套说明,便于按模块梳理微服务拆分、分库设计、分布式登录及第三方视频/存储接入等完整实现思路,适合希望从单体转向微服务开发、快速理解前后端分离协作模式的初中级开发者使用。

1. online_edu 在线教育系统拆解:这套 SpringCloud + Vue 微服务项目能直接用来做什么

把 online_edu 这套在线教育系统按 SpringCloud + Vue.js 微服务项目来拆,核心是覆盖在线教育产品的最小闭环:前台用户有课程、问答、文章三个入口,后台运营平台管内容、管数据、出报表。前后端分离是骨架,后端是 SpringBoot + SpringCloud + MyBatis-Plus + MySQL + Docker + Maven,前端是 Node.js + Vue.js,中间件还带 Redis、ActiveMQ、阿里云 OSS 和视频点播,报表用 ECharts,Excel 导入导出用 POI。对正在找 SpringBoot、SpringCloud 落地范例的人来说,这套代码最直接的价值,是能看一个真实项目怎么把服务拆分、数据持久化、异步消息、文件存储串成一条完整链路。适合刚学完 Spring、想上手微服务项目的 Java 从业者,也适合需要一套完整可跑系统的毕业设计。下面把技术栈和链路拆开说。

2. 服务划分与中间件选型:SpringCloud 微服务、MySQL、Docker 是怎么搭起来的

在动手看代码之前,先把架构讲清楚。online_edu 虽然叫“一个系统”,但落地到 Maven 工程里,是多个 SpringBoot 模块组成的。前后端分离是最外层的边界:Vue.js 工程负责页面渲染,SpringBoot 服务只开放 JSON 接口。

2.1 前台网站与后台运营平台:课程、问答、文章三条业务线

前台用户系统按业务分三块:课程负责课程分类、课程详情、视频播放、课程资料下载,问答负责用户提问、回答和采纳,文章负责图文内容展示。课程和文章的区别在内容形态:课程走“章节 + 视频 + 资料”的结构,文章走“富文本 + 标签”的结构,问答则是互动型业务,数据关系更复杂。这三块业务如果塞进一个单体工程里,后期改一个课程的字段,可能把文章和问答的服务一起重启,这在运营上完全不能接受。微服务化的直接收益就是三个模块各自开发、各自部署、各自扩容。

后台运营平台则是反过来的,它不直接面对 C 端用户,而是对课程、问答、文章做审核和管理。运营端主要处理课程上下架、文章置顶、问答审核,以及课程数据的统计分析。统计结果的展示用 ECharts,运营数据的 Excel 导入导出用 POI。前台用户看到的课程列表和后台运营端管理的课程字段基本共用一套 DTO,但需要把课程查询做成一个公共服务,前台和后台都走这个服务,再按角色过滤字段,避免运营数据被误打到前台接口里。

2.2 SpringCloud 微服务模块划分与 Gateway 路由

把工程展开后,典型结构是注册中心负责服务发现,网关做统一入口和路由转发,业务服务各自注册。我习惯把模块划分成下面这样:

模块职责关键依赖
服务注册中心服务注册与发现SpringCloud(Nacos 或 Eureka)
API Gateway统一入口、路由转发、跨域处理SpringCloud Gateway
course-service课程、分类、视频点播、课程资料MyBatis-Plus、OSS、视频点播 SDK
article-service文章管理、标签、关键字MyBatis-Plus、HttpClient
qa-service问答、评论、采纳MyBatis-Plus、Redis
admin-service运营后台、审核、统计、报表ECharts 数据接口、POI 导出 Excel

提示:原文里没有指定注册中心具体用 Nacos 还是 Eureka。我一般建议新手优先用 Nacos,控制台对注册列表、配置推送的展示更直观,配置中心还能少写很多环境差异配置。但照着 SpringCloud 官方示例学的话,用 Eureka 也完全能跑。

Gateway 路由的配置是这套系统里最容易踩坑的地方,先给一个常见的 path 路由写法:

spring: cloud: gateway: routes: - id: course-route uri: lb://course-service predicates: - Path=/api/course/** - id: article-route uri: lb://article-service predicates: - Path=/api/article/** - id: qa-route uri: lb://qa-service predicates: - Path=/api/qa/**

这个路由配置的要点是 uri 写成lb://,表示走客户端负载均衡去调用注册中心里的服务名,而不是直接写死 localhost。Path 断言用来区分同一端口下的不同前缀。为什么要统一走网关?如果不经过网关,前端要记住每个微服务的端口,CORS 也得各自配一遍,更麻烦的是跨服务调用时身份信息很难传递。网关里做一次统一的 token 校验,把用户信息写进请求头,下游服务直接读请求头,这个链路是最省事的。

2.3 Docker 部署 MySQL 与 Redis:容器化环境的“后悔药”

MySQL 在整套系统里是核心存储,Redis 主要负责缓存课程分类、热点文章和问答的会话。开发环境我习惯用 Docker 把这两个中间件先拉起来,避免本机装一堆东西,翻车了不用重装系统。一个 docker-compose.yml 就能起两个服务:

version: "3.8" services: mysql: image: mysql:8.0 container_name: edu-mysql ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: root123 TZ: Asia/Shanghai command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --default-time-zone=+08:00 volumes: - ./mysql-data:/var/lib/mysql restart: unless-stopped redis: image: redis:7-alpine container_name: edu-redis ports: - "6379:6379" volumes: - ./redis-data:/data command: redis-server --appendonly yes restart: unless-stopped

这里有几个参数值得说明。MySQL 的MYSQL_ROOT_PASSWORD是容器首次初始化时设置 root 密码的环境变量,只对第一次启动生效,所以很多人改密码改了没用,因为数据卷里已经初始化过了。--character-set-server=utf8mb4指定编码,避免插入 emoji 或中文生僻字时报 Incorrect string value 错误。时区参数TZ和--default-time-zone=+08:00一起设置,防止数据库时间与服务器时间差八小时。Redis 的--appendonly yes开启 AOF 持久化,防止容器重启后缓存数据全部丢失。

我一般会把这套 compose 文件放在项目根目录的 docker/ 目录下,执行docker compose up -d就能起服务。新手在装 MySQL 时经常会直接把宿主机已经占用的 3306 端口再映射一遍,然后报 Address already in use。遇到这种情况,先lsof -i:3306查占用再决定是停掉旧服务还是把容器端口换成 3307。血泪经验是这两条命令要在执行 docker compose 之前跑,不要在容器启动失败之后才开始查。

3. 后端实战:SpringBoot 整合 MyBatis-Plus、ActiveMQ 与视频点播

这一章进入代码层面。后端工程是标准的 Maven 多模块结构,父子工程统一管理版本,各个业务模块再引入自己需要的中间件依赖。对只想快速跑通系统的人来说,pom.xml 的版本对齐往往比业务代码更费神。

3.1 pom.xml 依赖与版本选择:springboot 版本太高引发的连环坑

先给父子工程的依赖框架。父 pom 用 dependencyManagement 统一版本,这是 Maven 多模块项目的基本素养,否则十个模块每个写一个版本的 SpringBoot,光依赖冲突就够喝一壶。一个常见的配置是:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <properties> <java.version>1.8</java.version> <spring-cloud.version>2021.0.8</spring-cloud.version> <mybatis-plus.version>3.5.3.1</mybatis-plus.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>

这里最需要注意的就是 SpringBoot 和 SpringCloud 的版本映射。Spring Cloud 官方给了一张版本对应表,2021.0.x 对应 SpringBoot 2.6.x/2.7.x,2022.0.x 对应 SpringBoot 3.0.x。很多人直接去 Maven 仓库拉最新版 SpringBoot 3.x,结果 SpringCloud Gateway 的依赖引入之后类都找不到。这就是常说的 springboot 版本太高带来的问题:3.x 把javax.*改成了jakarta.*,MyBatis-Plus 在 3.5.3 之前用的是 javax,老用法直接编译失败。如果你拿到的 online_edu 源码是 2.x 时代的写法,最稳的是沿用 2.7.x 而不是升到 3.x。

另一个容易忽略的是 Lombok 和 MyBatis-Plus 的配合。新版 MyBatis-Plus 需要 Lombok 在编译期生成 getter/setter,如果你把 Lombok 从依赖里剔掉了,MyBatis-Plus 的实体类在反射时会拿不到属性,查出来的 Map 和实体类对不上,这种报错非常难排查。

3.2 application.yml 多环境配置:端口、数据源、Redis

多环境放在 application.yml 里管理,实际项目中用 spring.profiles.active 区分 dev、test、prod 三个环境。核心配置长这样:

spring: profiles: active: dev --- spring: config.activate.on-profile: dev datasource: url: jdbc:mysql://localhost:3306/online_edu?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 activemq: broker-url: tcp://localhost:61616 user: admin password: admin cloud: nacos: discovery: server-addr: localhost:8848 mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.edu.online.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl server: port: 8081

连接串里useSSL=false这个参数要重点记,后面避坑章节会专门讲 MySQL 的 SSL 连接错误。serverTimezone=Asia/Shanghai指定时区,避免驱动 8.0 之后不认默认时区导致差八小时。map-underscore-to-camel-case打开下划线转驼峰,数据库字段course_name能自动映射到courseName,不用手写一堆 resultMap。log-impl配成 StdOutImpl 是为了在控制台直接看到 SQL,新手调试阶段建议保留,等上生产再关掉。

SpringBoot 自带默认数据源 HikariCP,不需要额外配连接池。如果你的工程里还引入了 DBCP 或 C3P0 的依赖,要手动排除,否则启动时会随机挑一个连接池实现,行为表现完全不可控。

3.3 MyBatis-Plus 实现课程分页与条件查询

MyBatis-Plus 在这套系统里的定位是简化单表 CRUD。课程列表的典型查询是:按分类筛选 + 按关键字模糊匹配 + 按销量排序 + 分页。用 MyBatis-Plus 的 QueryWrapper 写出来非常短:

@RestController @RequestMapping("/api/course") public class CourseController { @Autowired private CourseService courseService; @GetMapping("/page") public IPage<Course> page(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit, @RequestParam(required = false) Long categoryId, @RequestParam(required = false) String keyword) { Page<Course> pageParam = new Page<>(page, limit); LambdaQueryWrapper<Course> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(categoryId != null, Course::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Course::getCourseName, keyword) .orderByDesc(Course::getBuyCount); return courseService.page(pageParam, wrapper); } }

这段代码的逻辑是:先构造 Page 对象,LambdaQueryWrapper 用 lambda 表达式指向实体字段而不是写列名,字段名改了编译器直接报错,不会等运行时才返回空数据。eq方法的第一个参数是布尔条件,categoryId 为空时直接跳过这个条件,不用写 if 判断。like同理,keyword 没传就不拼模糊查询。最后orderByDesc按销量倒序排。返回值用IPage<Course>是为了把 total、pages 这些分页元数据一起返回给前端。

这里要补一个关键点:MyBatis-Plus 的分页不是开了就有的,需要注册分页拦截器。很多人复现项目时发现 page 方法返回的 total 一直是 0,或者 SQL 里没有 LIMIT,就是没配这个拦截器:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

PaginationInnerInterceptor传DbType.MYSQL是让插件生成 MySQL 方言的分页 SQL。如果你把数据库切到 PostgreSQL 还传 MYSQL,分页 SQL 会按 MySQL 语法拼,直接报语法错误。

3.4 ActiveMQ 异步解耦:视频发布与审核消息队列

视频点播是这套系统里最容易理解异步消息价值的地方。运营在后台发布一门课程,上传视频后如果立刻做转码、审核,同步接口会长时间阻塞,前端体验就是转圈圈。实际项目中用 ActiveMQ 做解耦:上传接口只管把视频文件存到阿里云 OSS、拿到视频 ID,然后往消息队列里丢一条“视频上传完成”的消息,转码和审核由监听器异步处理。

生产者的写法大致这样:

@Service public class VideoPublishService { @Resource private JmsMessagingTemplate jmsMessagingTemplate; public void publishVideo(VideoInfo videoInfo) { // 先存业务数据,保证后续查询能看到记录 videoInfoMapper.insert(videoInfo); // 发送消息,通知转码/审核模块异步处理 VideoPublishEvent event = new VideoPublishEvent(); event.setVideoId(videoInfo.getId()); event.setOssUrl(videoInfo.getOssUrl()); jmsMessagingTemplate.convertAndSend("video.publish.queue", event); } }

消费者的核心逻辑:

@JmsListener(destination = "video.publish.queue") public void handlePublish(VideoPublishEvent event) { // 这里做视频转码或调用第三方审核接口 boolean success = videoTranscodeService.transcode(event.getOssUrl()); if (success) { videoInfoMapper.updateStatus(event.getVideoId(), 1); } else { videoInfoMapper.updateStatus(event.getVideoId(), -1); } }

关键点在于,消息里不要带整个业务的完整数据,只带 videoId 和 OSS 的 URL,消费者需要更多信息时再查库。为什么不直接同步调用转码服务?因为转码服务不可控,第三方 API 可能出现几十秒的延迟,同步调用会拖垮上传接口的吞吐量。发送消息基本是毫秒级操作,接口能立刻返回,这就是解耦的价值。

ActiveMQ 的注意点集中在 broker-url 和连接方式上。SpringBoot 集成 ActiveMQ 时,如果spring.activemq.in-memory=true,消息会自动落在一个内存 broker 里,一旦应用重启消息全丢。联调阶段无所谓,但如果你发现重启后消息不见了,先看这个开关是不是被写成了默认 true。

4. 前端 Vue.js 与 ECharts:前后端分离、代理转发与打包部署

后端接口就绪后,进入前端的部分。online_edu 的 Vue.js 工程分成前台门户和后台运营两个入口,但它们共用一套组件库和请求封装。前端拿到的接口地址是网关地址,开发环境走 webpack 的 devServer 代理,生产环境则直接把 dist 打包产物交给 SpringBoot 或 Nginx 托管。

4.1 Vue 工程初始化与接口代理

拿到 Vue 工程以后,先不急着看业务页面,把环境跑起来。npm install是第一个坎,建议 Node.js 版本保持在 16 或 18,webpack 4 和 node 18 容易有 OpenSSL 相关的报错,这时需要修改 NODE_OPTIONS 参数。工程里最关键的配置文件是 vue.config.js,接口代理就写在这里:

// vue.config.js module.exports = { devServer: { port: 9528, // 前端开发服务端口 proxy: { '/api': { target: 'http://localhost:8080', // 网关地址 changeOrigin: true, // 透传 Host 头 pathRewrite: { '^/api': '/api' } } } } }

changeOrigin: true的意思是把请求头里的 Host 改成 target 地址,很多后端服务会根据 Host 做校验,不设置会偶发 403。pathRewrite里我保留了/api前缀,因为网关的 Predicate 就是按/api/**匹配的,如果前端把前缀去掉了,网关直接返回 404。这里不建议用'^/api': ''这种重写,除非后端接口本来就不带 /api 前缀。

4.2 把 vue 打包放进 SpringBoot 中:合并部署的两种做法

前后端分离的开发环境是理所当然的,但真正的坑在部署阶段。在服务器资源有限的环境中,最常见的做法是“把 vue 打包放进 SpringBoot 里”,也就是把前端构建产物放到 SpringBoot 的静态目录下,然后一个进程对外提供页面和接口两种服务。操作分三步:

# 1. 前端工程根目录执行构建 npm run build # 2. 把构建产物复制到后端静态目录 cp -r dist/* ../online_edu-admin/src/main/resources/static/ # 3. 重新打包并启动 SpringBoot 服务 mvn clean package -DskipTests java -jar online_edu-admin/target/online_edu-admin.jar

复制完 dist 之后,目录结构大概是:

src/main/resources/ ├── static/ │ ├── index.html │ ├── css/ │ ├── js/ │ └── favicon.ico └── application.yml

SpringBoot 的静态资源匹配是有顺序的,默认先匹配classpath:/static/,命中不了再走 Controller。这意味着如果你在 Controller 里写了一个@RequestMapping("/index.html"),它会优先被静态目录的 index.html 拦截。还有一个更隐蔽的问题:前端路由如果是 Vue Router 的 history 模式,刷新/article/123时 SpringBoot 找不到对应的静态文件,直接 404。解决办法是写一个 forward 的 Controller:

@Controller public class SpaForwardController { @RequestMapping(value = {"/{path:[^\\.]*}", "/**/{path:[^\\.]*}"}) public String forward() { return "forward:/index.html"; } }

这个正则的意思是不带点的路径都转发到 index.html,避免图片、JS 文件被误转发。如果你用的是 hash 模式的路由,则不存在这个问题,所以初次跑通项目时先切回 hash 模式能省不少时间。

4.3 ECharts 课程数据看板:图表配置与后端 API 对齐

后台运营平台的首页通常是一个数据看板,用 ECharts 渲染课程销量、用户活跃度、问答趋势这些数据。前端画图本身不难,难的是和后端接口的返回结构对齐。我比较习惯约定一个统一的图表数据结构,后端返回菜单级的 JSON:

{ "dates": ["2026-04-01", "2026-04-02", "2026-04-03"], "series": [ { "name": "课程销量", "data": [102, 134, 90] }, { "name": "新增用户", "data": [45, 78, 66] } ] }

前端 ECharts 的核心配置长这样:

// 课程销量趋势图 chart.setOption({ tooltip: { trigger: 'axis' }, legend: { data: ['课程销量', '新增用户'] }, xAxis: { type: 'category', data: resp.dates }, yAxis: { type: 'value' }, series: resp.series.map(item => ({ name: item.name, type: 'line', smooth: true, // 开启平滑曲线 data: item.data })) });

这里的重点是tooltip的 trigger 设为 axis,鼠标滑过某个日期时把所有系列的值一起显示,如果是单条数据用 item 更合适。legend的 data 要和 series 的 name 一一对应,否则图例显示不出来。如果课程数据量大,比如一年 365 天,x 轴的刻度会挤成一团,常见做法是在 xAxis 上配axisLabel: { interval: 30 }让标签每 30 条显示一次,或者用 dataZoom 组件做缩放。数据量处理是 ECharts 环节最常见的翻车点:后端一次性吐 365 个日期,前端不做 dataZoom,图表看起来就是一条黑带。加了 dataZoom 之后,用户能自己拖拽看区间,体验完全不同。

5. 避坑指南:从 Docker 到 MySQL 的常见问题与排查记录

这部分内容是我在拆这个项目以及带团队复现时踩过的真实问题,每一条按“现象 → 原因 → 解决”的顺序写。

5.1 docker 安装 mysql 失败:容器启动即退出的排查

现象:Docker 里拉取 mysql:8.0 镜像后执行 docker run,容器状态永远显示 Exited,docker logs 只有几行初始化日志就停了。

原因:八成是权限或目录的问题。MySQL 8.0 的镜像内部用 mysql 用户跑服务,如果把宿主机目录挂载到 /var/lib/mysql,但宿主机目录权限是 root 所有,容器内服务无法写入,启动直接失败。另一个高频原因是端口被宿主机已有服务占用,比如本机装了 MySQL 又在跑 3306。

解决:先docker logs container_id看最后几行日志,如果是 Permission denied,则在挂载目录上执行chown -R 1000:1000 ./mysql-data之后再启动。如果日志显示 Address already in use,说明端口冲突,把宿主机映射端口改成 3307 或者先停掉本机 MySQL。从那以后,我每次启动容器前都先执行docker ps -a和lsof -i:3306检查一遍。

5.2 MySQL SSL 连接错误:连接串里加哪个参数

现象:SpringBoot 项目启动时,日志里出现Communications link failure或者SSL connection error: protocol version mismatch,在 mysql 8.0 客户端驱动连接 MySQL 5.7 或某些云数据库时偶发。

原因:MySQL 8.0 之后 JDBC 驱动默认开启 SSL 连接,服务端的 SSL 证书或协议跟客户端不对齐时,握手阶段直接失败。这是纯粹的配置不一致问题,不是连接被拦截。

解决:在 JDBC 连接串中显式加useSSL=false,并且可以加allowPublicKeyRetrieval=true。这两个参数在开发环境没有影响,生产环境如果确实需要加密连接,则改成useSSL=true并配合sslMode=VERIFY_CA和证书路径。很多项目从 5.7 升到 8.0,连接串里没动,启动后就开始玄学报错,其实只要这两个参数加上,不用改代码就能解。从那以后,我只要写 MySQL 连接串就显式把 useSSL 写出来,不依赖驱动默认值。

5.3 SpringCloud 服务注册不上:Nacos 地址与网络命名空间的问题

现象:网关和业务服务都启动了,但网关转发请求时报 503,到注册中心控制台一看,业务服务压根不在服务列表里。

原因:大多数情况下是 Nacos 的 server-addr 写错了,或者业务服务在容器里跑、Nacos 在宿主机跑,服务里配置的 localhost 指向容器内部,找不到 Nacos。还有一种情况,application.yml 里配置了 server-addr,但启动参数里又覆盖了另一个地址,两个不一致。

解决:先确认网络互通:在容器内执行telnet nacos主机名 8848,能通再看服务配置。配置统一收敛到 bootstrap.yml 里,并检查启动日志里 Nacos 注册成功那一条。如果是在 Docker 容器中,需要把 localhost 改成宿主机 IP 或 Docker 网络中的服务名。排查这类问题的顺序是:先 ping 通不通,再看日志有没有 Nacos registry 记录,最后才怀疑代码。

5.4 IDEA 2026 配置 SpringBoot 启动端口:编辑配置数据里的端口改不动

现象:在 IDEA 里用 spring-boot:run 启动多个微服务模块,在 Edit Configurations 的 VM options 里填了端口参数,启动后还是用 application.yml 里的端口。

原因:IDEA 的 spring-boot:run 属于 Maven 插件执行,端口优先级来自spring-boot-maven-plugin的 jvmArguments,而不是 VM options。如果你在 application.yml 里写了 server.port,在 Program arguments 里没有显式传--server.port=8083,那么 VM options 里的-Dserver.port=8083可能被 application.yml 覆盖,也可能被 Maven 插件忽略。

解决:最稳妥的方式是在 Edit Configurations → Spring Boot → Program arguments 里显式传--server.port=8083。或者直接用不同环境 profile:--spring.profiles.active=dev,dev 配置里写各自不同的端口。从那以后,我再遇到“端口明明改了却不变”的问题,都先回去查 Program arguments,而不是改 VM options。

5.5 前端接口 404:代理配置对了但页面还是拿不到数据

现象:Vue 工程启动后,浏览器访问前端页面正常,但 Network 面板里所有 /api 开头的请求全部 404,直接在后端接口地址访问却是通的。

原因:前端 devServer 代理取 vue.config.js 里的配置,后端网关预期的是 /api 开头。一旦中间某层把 /api 前缀改掉了,网关路径匹配不上,就返回 404。另一个隐蔽原因是 SpringBoot 的静态资源处理和 Controller 映射冲突,比如后端写了一个/api/course/*的接口,返回值被静态资源处理器误拦截。

解决:把代理配置改为保留 /api 前缀,并确保网关的路径断言也是 /api 开头。同时在 SpringBoot 侧把所有接口统一放到 /api 命名空间下,静态目录只放前端构建产物。排查时先看 Network 里请求的完整 URL,从真实 URL 反推是哪一层改动了路径,比从头看代码快得多。

6. 用脚本验证全部服务状态:Actuator 健康检查与端口探活

模拟一个真实场景:你刚从 Git 仓库拉下来一套 online_edu 工程,第一次启动想确认几层依赖之间的关系对不对。与其一个个浏览器打开试,不如用一个脚本把“中间件 → 网关 → 业务服务”三层全部探一遍。我习惯在 bin 目录放一份。

check_http() { curl -s -o /dev/null -w "%{http_code}" "http://localhost:$1$2" } check_port() { nc -z -w 2 localhost "$1" && echo "[OK] $1" || echo "[FAIL] $1" } echo "== 中间件 ==" check_port 3306 # MySQL check_port 6379 # Redis check_port 61616 # ActiveMQ echo "== 网关 ==" echo "gateway -> $(check_http 8080 /api/course/index)" echo "== 业务服务 ==" for p in 8081 8082 8083 8084; do echo "$p -> $(check_http $p /actuator/health)" done

逻辑说明:nc -z -w 2探测端口是否开放,2 秒超时避免卡死。check_http用 curl 拿 HTTP 状态码,200 表示服务正常,503 说明服务还在启动中,000 说明端口没起来。循环把四个业务服务端口挨个探一遍。如果你发现网关能通但业务服务 503,通常意味着业务服务还没完成注册,或者健康检查没通过。

实际边界是,如果工程里没有引入 Actuator,/actuator/health会返回 404。此时可以直接看网关转发一个真实接口的状态码,比如 GET /api/course/index。所以使用 Actuator 之前要先确认依赖spring-boot-starter-actuator存在,而这个依赖在 online_edu 的 admin-service 里一般是配好的。

我的建议是把它作为每次启动项目的第一道工序:先跑这个脚本,把中间件连通性、网关状态、业务服务健康状态三行结果同时摆在面前。从那以后,我每次换机器重新部署这套系统时都强制走一遍这个脚本,再也不会出现“网关起来了、MySQL 没起来、前端疯狂转圈”的尴尬局面。希望这份拆解笔记能帮到你。

本文还有配套的精品资源,点击获取

返回列表