
前阵子帮一个计算机专业的朋友做了套毕业设计课题正好是“计算机精品课程在线学习互动系统”要求不高但东西却很杂既要跑得起来做演示又得有微服务元素还得带小程序端和聊天互动。做完之后我最大的感受是这类项目真正难的并不是某个单独技术而是怎么把SpringBoot、SpringCloud、Vue、小程序这一整套东西串起来并且能让你在答辩现场稳定演示。这篇文章就基于这一整套项目把我从架构设计到部署上线的完整思路、具体配置、踩坑实录都梳理一遍希望能给正在做类似系统或者想学习微服务小程序开发的同学一点参考。这套系统最终实现的功能大概是这样后台用SpringBootSpringCloud拆分成用户、课程、学习记录、聊天等几个核心微服务前台是Vue搭建的管理端和微信小程序端的学员端学员可以浏览课程、观看视频、提问互动、参与实时聊天管理员可以管理课程资源、审核问答、查看学习数据。演示时用Docker Compose一键拉起整套环境聊天则通过WebSocket实时通信。听起来东西不少但真正理解架构以后其实是很有章法可循的。1. 项目整体架构与微服务拆分思路1.1 为什么选微服务而不是一个单体打开这个项目标题“微服务分布式”这个词已经锁定了架构方向。但我要说的是如果只是为了做个演示系统单体SpringBoot项目30分钟就能搭起来为什么还要拆微服务最核心的答案是拆出来的每个服务都是独立的业务域你可以分别演示SpringCloud的注册发现、负载均衡、声明式调用、网关路由等特性这些才是微服务项目的真正考点。就这套系统而言我拆了4个基础服务和2个应用入口用户服务负责登录鉴权和学员信息维护课程服务负责课程分类、课程详情和视频信息学习服务负责学习记录、笔记、问答聊天服务负责在线聊天和消息持久化。管理端的Vue应用访问网关小程序端同样走网关好处是所有请求统一经过认证和路由代码里不需要关系服务具体部署在哪台机器上。另外从开发角度看拆开之后的好处非常明显。聊天服务因为要维持WebSocket长连接它的负载情况和用户服务完全不同单独拆出来以后可以做独立的水平扩展不至于因为聊天流量把课程查询接口拖垮。这就是微服务最本质的价值按业务的资源需求、发布频率、故障爆炸半径来切分系统。1.2 核心微服务模块的划分细节划分服务的时候我有一个原则能独立完成一个业务闭环的模块才值得拆成服务。举例来说“课程服务”不只是存课程表它还包含课程分类、课程标签、讲师信息甚至视频资源地址的维护这是因为它要支撑课程列表和详情页的完整展示。而“学习服务”则包含学习记录、笔记和问答原因是这些数据都围绕“学员学习行为”这个业务域。服务之间通过OpenFeign进行声明式调用比如查询课程详情时学习服务需要根据课程ID获取该课程的学习人数这时就可以通过Feign调用课程服务接口。为了避免服务间调用影响性能我在课程服务里缓存了热门课程的基本信息学习数量这类实时性不高的数据其实也可以定期同步到缓存中。微服务拆分最常见的误区是拆得太碎。我见过有人把用户表和角色表拆成两个服务结果一个登录操作要来回调三次接口延迟高得离谱。面向教学演示的系统服务粒度做到“用户、课程、学习、聊天”已经是合理的上限。1.3 技术选型的几个理由后端框架锁定了SpringBoot SpringCloud原因很简单SpringBoot让你写业务代码足够快SpringCloud的组件覆盖了分布式系统绝大部分痛点。注册中心用Nacos为什么没选Eureka因为Nacos除了服务注册发现还自带配置中心可以顺便演示配置动态刷新。网关用的是Spring Cloud Gateway因为它是基于WebFlux的响应式网关性能比Zuul 1.x好不少而且官方还在持续维护。前端管理端用Vue 3 Element Plus小程序端就是原生微信小程序。聊天部分如果只想做“演示级别”的聊天其实用简单的轮询也能做但为了体现分布式通信我用了WebSocket 消息队列的经典组合把聊天消息发给消息中间件再由聊天服务异步推送给前端同时落库。这里不选WebSocket而是“WebSocket MQ”是为了演示微服务之间通过消息解耦的思路。2. 后端服务搭建与关键配置2.1 SpringBoot基础工程与统一配置这部分我从实际创建工程讲起。每个微服务都是一个独立的SpringBoot工程命名上我用的是course-server-user、course-server-course这类结构一眼能看出归属。为了不重复写依赖版本我用Maven聚合工程管理所有服务父POM统一锁定SpringBoot版本和SpringCloud Alibaba版本。这里提醒一个重要细节SpringBoot版本和SpringCloud Alibaba版本必须严格对应否则启动时会碰到各种莫名其妙的Bean注入失败。以我的项目为例用的是SpringBoot 2.7.x SpringCloud Alibaba 2021.0.5.0这套组合已经过了大量项目检验。每个服务里我都加了统一的配置类最少包含数据库连接、Redis连接、Nacos注册地址、Feign超时时间等。以课程服务为例配置片段大致是这样spring: application: name: course-server-course cloud: nacos: discovery: server-addr: ${NACOS_ADDR:127.0.0.1:8848} datasource: url: jdbc:mysql://${MYSQL_HOST:127.0.0.1}:3306/lms_course?useUnicodetruecharacterEncodingutf8 username: root password: ${MYSQL_PASSWORD:root}所有密码和IP地址都通过环境变量覆盖这样同一个包换一套环境变量就能跑不用改代码。这也是一个容易被忽视的演示要点评审老师问“你部署的时候怎么改配置”的时候直接说容器环境变量专业感一下就上来了。另外每个服务都引入了Spring Boot Actuator暴露health端点。这个方法在排查的时候特别好用某个服务起不来先看Nacos里有没有注册再看/actuator/health返回的状态。因为微服务数量多了以后很难用肉眼判断哪个服务挂了健康检查是最基本的可观测性手段。2.2 Nacos注册中心与Gateway网关配置注册中心我选了Nacos启动方式特别简单官方给了Docker镜像拉下来直接跑docker run -d --name nacos -p 8848:8848 -p 9848:9848 -e MODEstandalone nacos/nacos-server:v2.2.3这里端口要注意8848是HTTP接口9848是gRPC接口SpringCloud客户端会默认连接9848如果防火墙只放行了8848服务注册会失败。这是个经典坑点我在部署到云服务器的时候踩过云安全组必须同时放行两个端口。Gateway网关是整个系统对外唯一的入口集中处理跨域、路由和登录鉴权。我把路由配置放在application.yml里最常见的一段是这样spring: cloud: gateway: routes: - id: course-route uri: lb://course-server-course predicates: - Path/api/course/** filters: - StripPrefix1lb://前缀表示从Nacos中按服务名负载均衡后面接服务名称不需要写具体IP。所有请求先到网关网关根据路由规则转发到对应服务这种模式也解决了前端跨域的问题因为前端只需要跟网关一个源打交道。登录鉴权我放在网关全局过滤器里处理。请求头里带Authorization令牌网关校验通过之后再把用户ID添加到请求头转发给下游服务。这里推荐用简单的JWT而不是把所有服务都接入Spring Security因为服务间内部调用不应该受安全框架束缚。JWT校验逻辑非常简单解析token看签名和过期时间。为了不把认证逻辑塞到网关里太臃肿我做了一个微服务里的公共starter网关和其他服务都能引用只是各自使用不同的功能。2.3 服务间通信与分布式事务权衡服务之间通信用的是OpenFeignFeign的优点是把远程调用变成本地接口调用代码可读性很高。比如学习服务要查询某个课程的学习人数定义这样一个FeignClientFeignClient(name course-server-course, path /course-inner) public interface CourseClient { GetMapping(/{courseId}/learn-count) LearnCountDTO getLearnCount(PathVariable(courseId) Long courseId); }注意这里路径我特意加了/course-inner前缀表示这是内部接口不暴露给前端。内部接口通常不走网关或者用网关过滤掉这样防止外网直接访问服务数据。这是微服务之间通信的一个常见安全设计很多初学者容易忽略。关于分布式事务这是微服务里最容易翻车的点。我这套系统里用户提交学习记录可能同时要更新统计计数如果跨服务做严格事务就要引入Seata。但演示项目我不想把复杂度拉太高所以采用了“最终一致性”的思路核心数据写入本地库统计类的操作通过异步消息或者Feign调用后再补偿。具体场景是学员完成一个视频学习后学习服务先保存自己的记录然后发送一条消息到消息队列课程服务的统计模块去消费消息、更新学习人数。这个MQ队列可以选RocketMQ也可以选RabbitMQ我因为部署环境用的Docker选了RabbitMQ启动简单、内存占用小。消息驱动的另外一个好处是即便课程服务统计模块短暂不可用消息会堆积在队列里等它恢复后还能继续消费不会丢失数据。这在演示的时候也是一个很好的讲解点微服务不是不能用分布式事务而是要区分场景能用最终一致性解决的不要强上强一致方案。3. 前端Vue与小程序双端实现3.1 Vue管理后台的课程与用户管理管理端用了Vue 3 Vite Element Plus。Vite启动速度快开发体验比Webpack舒服太多。这个后台主要给管理员维护课程基本信息和审核问答界面不追求花哨但要保证操作顺畅。我的布局很简单左侧菜单分成“课程管理”“用户管理”“问答管理”“数据统计”四个模块。课程管理页面的核心是一个列表页加一个编辑弹窗。列表页展示课程封面、名称、分类、价格如果涉及收费、上下架状态。因为课程封面图片数量多我把图片上传到七牛云或者阿里云OSS数据库只存URL。这里有个经验演示环境最怕上传功能依赖外网云存储万一现场没网就很尴尬。所以我在项目里做一个本地存储的实现图片上传保存到网关所在服务器的指定目录同时用网关映射一个静态资源路径来访问。这样既可插拔又能应对离线环境。用户管理页主要展示学员列表和角色信息。这里我偷了个懒学员和课程用的是同一个用户服务但小程序端注册的用户默认是学员角色。管理员可以通过这个页面查看用户活跃时间、学习时长等。数据统计页展示核心指标比如新增用户趋势、课程学习次数TOP10、每日活跃人数。图表用的是ECharts通过Vue组件封装一下数据由课程服务和学习服务提供聚合接口。3.2 微信小程序端的页面结构与核心交互小程序端面向的是学员结构相对简单首页、课程列表、课程详情、个人中心、聊天页。首页是精品课程的推荐橱窗我从课程服务拉取推荐列表调整了下发顺序把“精品课程”的标签和评分高的课程放前面。课程列表支持分类切换和关键词搜索这里做了一个很常规的分页加载——每次上拉触底加载下一页避免一次加载太多数据导致小程序白屏。课程详情页是小程序端最复杂的页面。包含课程简介、目录列表、讲师介绍和评价还可以试看视频。视频播放用的是小程序自带的video组件src指向课程服务返回的播放地址。如果课程是直播回放或m3u8流需要先通过服务端接口解析出低码流地址再传给video组件这就是热搜词“vue播放m3u8免安装”的对应场景。在小程序里iOS端对HLS流的兼容性比Android更好实测下来MP4格式最稳所以演示环境我优先上传了一段mp4资源作示例。小程序端登录是典型的微信授权登录流程前端调用wx.login拿到临时code发送到用户服务用户服务再调用微信接口换取openid和session_key然后生成系统自己维护的JWT返回给小程序端。这里要注意从后台换openid的请求必须走服务端不能在小程序里直接处理否则AppSecret泄露会带来安全隐患。3.3 聊天互动与实时消息的实现思路聊天是这个系统的特色功能标题里专门有“聊天”两个字。我设计的是一个课程讨论区 一对一私聊的混合模式。最核心的聊天功能放在聊天服务里使用Spring WebSocket作为消息通道配合STOMP协议做消息路由。前端小程序通过socket模块建立WebSocket连接消息格式使用JSON。聊天服务启动时在Nacos注册同时暴露WebSocket端点。因为网关本身是WebFlux网关代理WebSocket需要特殊配置。我的做法是让WebSocket请求不经过网关小程序直接连接聊天服务的独立端口。这样演示时虽然多暴露一个端口但更稳定也更容易定位问题。如果你希望所有请求都统一走网关也可以配置Gateway的WebSocket路由格式大概是- id: chat-ws uri: lb:ws://course-server-chat predicates: - Path/ws/**聊天实时性这里我用的是内存版消息队列加STOMP的channel做消息分发。用户发送消息时聊天服务接收后存库同时通过SimpMessagingTemplate推送到对应订阅目标。单聊的话每个用户订阅一个个人通道群聊的话每个课程讨论组订阅一个课程通道。复杂的地方在于用户连接时需要根据JWT中的用户ID建立会话并在断线时清理会话。这段逻辑我建议放在HandshakeInterceptor里做避免WebSocket连接被未认证用户滥用。聊天消息落库表结构也很简单消息ID、会话ID/课程ID、发送人、消息内容、消息时间、是否已读。我加了last_message冗余字段在会话表里这样聊天列表页直接显示最后一条消息再也不用每次聚合查询消息表。4. 课程资源处理与在线播放技术4.1 视频课程的上传、存储与播放地址生成课程视频是最占资源的部分。我处理课程视频的方式是直接存在本地磁盘分目录数据库存相对路径和播放URL。对于mp4格式SpringBoot可以通过静态资源映射直接提供访问但更稳妥的方法是做一个独立的文件服务或者把视频资源放到OSS/MinIO。MinIO是开源的对象存储和S3协议兼容部署一个单节点也很方便docker run -d -p 9000:9000 -p 9001:9001 minio/minio server /data --console-address :9001把视频传到MinIO以后生成带有效期的预览URL前端拿到这个URL就可以播放。这样做有一个额外的好处演示时可以说“这里用了对象存储做海量视频资源的存储”比本地磁盘方案在架构上更有说服力。视频播放还有一个细节微信小程序的video组件的src只接受网络地址而且Android和iOS对视频编码格式要求有差异。最简单的做法是服务器端准备一个mp4格式的样例视频用H.264编码实测在Android和iOS的微信小程序里都能流畅播放。如果你有多个码率的视频文件可以在后端做一个接口根据用户网络类型返回对应的优质码流这一步可以作为扩展点提一下。4.2 富文本课件与在线问答模块设计除了视频课程详情往往会带富文本课件、图文讲义等内容。富文本数据存储用LongText字段在前端展示时通过Vue的v-html渲染或者在小程序端用rich-text组件。需要注意的是富文本里的图片不能直接用外链否则很容易因为防盗链而显示失败我在后端对富文本内容中的图片地址做了统一替换全部改为本站或对象存储的完整地址。互动问答模块是我觉得最体现“互动系统”价值的地方。学员针对课程章节可发起提问管理员或讲师可以在管理后台回答学管理后端使用同一套微服务接口避免重复造轮子。问答列表需要展示提问时间、状态、回答数提问详情包含问题和回答内容。我设计了两个状态待回答和已回答后台支持按状态筛选。这一块在答辩时很容易引起关注因为它是实打实的业务闭环。为了提升互动感我在学员端自己做了一个“常见问题”的自动匹配。当学员输入问题时先通过关键词匹配快速返回FAQ内容如果匹配不到再进入人工问答环节。这算是一个极简的智能客服雏形也可以作为后续扩展为AI问答的引子。4.3 弱网环境的适配与演示稳定性保障在线学习系统最怕现场演示时网络抖动。为了保障演示稳定我做了三层热度优化图片资源走浏览器/小程序缓存接口响应做Redis缓存视频播放用Range分段加载。SpringBoot内置的spring-resources对Range请求默认支持这样视频播放器可以拖动进度条。图片缓存我让网关响应头带上Cache-Control: max-age3600减少重复请求。Redis缓存主要用于课程详情、课程分类等高频只读接口缓存key用course:detail:{id}手动管理过期时间。这里有一个非常重要的坑如果课程数据更新了一定要及时删除对应缓存否则学员看到的信息是脏数据。我在课程服务更新课程后直接调用Redis删除相关key简单有效比任何复杂的缓存一致性方案都更适合这种小型系统。在演示环境我还把所有外部依赖尽量内聚到一台Docker Compose里包括MySQL、Redis、Nacos、RabbitMQ、MinIO。这样就算局域网内演示也可以保证所有服务数据读在本机外部网络断开不影响演示主体功能。唯一需要注意的是如果你要演示微信小程序登录或视频播放那还是要保证出网正常的。5. 演示部署与常见问题排查5.1 Docker Compose一键拉起演示环境演示最怕现场装环境。我把整套依赖都容器化了写了一个docker-compose.yml包括数据库、缓存、消息队列、注册中心、对象存储并且把业务服务用Dockerfile打进镜像。最终的编排文件大致分为依赖区和应用区services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: lms ports: - 3306:3306 redis: image: redis:7 ports: - 6379:6379 rabbitmq: image: rabbitmq:3-management ports: - 5672:5672 - 15672:15672 nacos: image: nacos/nacos-server:v2.2.3 environment: MODE: standalone ports: - 8848:8848 - 9848:9848然后业务服务通过Maven打包成jar再用Dockerfile构建镜像。Dockerfile很简单FROM openjdk:8-jre-alpine COPY target/*.jar app.jar ENTRYPOINT [java, -jar, /app.jar]使用docker compose up -d把所有服务拉起来然后再手动启动业务服务的容器或者直接写成depends_on。值得强调的一个问题容器内服务访问数据库服务的地址不能再用127.0.0.1而是要用compose里的服务名mysql否则连接不上数据库。为了让启动更简单我写了初始化SQL脚本建库建表语句放在启动时自动执行。SpringBoot的spring.sql.init可以在启动时执行schema.sql和data.sql但要注意用MySQL8和continue-on-error参数避免重复执行报错。实际演示过程中重启容器后数据不会丢因为MySQL数据卷已经映射到宿主目录了这对保留演示数据特别关键。5.2 高频出现的启动报错与定位技巧这类项目部署最常出问题的不是业务代码而是服务之间互相等着起来。我遇到过课程服务启动时去连NacosNacos还没就绪导致注册失败另一个服务因为数据库连接未准备好就直接启动导致初始化失败。解决思路是控制启动顺序或者给每个服务容器加restart: unless-stopped让服务自动重试。如果你的业务服务里有初始化逻辑最好做一个失败回退比如用spring.cloud.nacos.discovery.fail-fastfalse。在排查问题时我依赖两个“一眼定位”的方法。第一是看Nacos控制台的服务列表五个服务注册上了几个哪个没上去一眼就清楚。第二是看服务日志里的关键报错最常见的错误是java.net.UnknownHostException说明服务间使用服务名调用但Nacos没有解析到其次是Connection refused通常是被调服务没有启动或者端口不对。这两个占了我平时调试的八成工作量。还有一个容易踩的坑是Feign调用超时时间设置。默认情况下Feign超时是1秒如果某个接口里做了一次Redis查询和一次数据库查询复杂度稍微高一点就可能在网关转发时超时导致前端报504。我统一在配置里加长了超时时间feign: client: config: default: connectTimeout: 3000 readTimeout: 5000同时也建议给Gateway路由的HttpClient设置连接池和响应超时。否则压力稍微大一点同样会报网关超时。5.3 性能优化与后续扩展方向这套演示系统虽然以跑通为目标但性能优化该做的还是做了一些。最有价值的一个优化是Redis缓存穿透和击穿的处理。课程热门详情在并发高的时候如果缓存刚好过期大量请求会直接打到数据库。我用了一个极简的“逻辑过期”方案缓存里不仅放业务数据还放了一个过期时间戳后台定时任务提前把热点数据刷入Redis如果查询时发现数据已经逻辑过期就先返回旧数据同时触发异步刷新。这样既不会击穿数据库演示时还能讲清楚“缓存雪崩、穿透、击穿”这些概念。后续如果有时间这个系统可以做三个很有意义的扩展。一是把问答模块接上大模型做一个课程知识库的自动答疑二是增加学习路径推荐根据用户的学习历史和互动频率推送合适的课程三是把视频播放换成HLS直播流这样可以在线组织直播公开课聊天室也能变成直播弹幕。这三个方向都是当前在线教育比较热门的功能如果能在答辩时说出规划会显得项目有前瞻性。小程序端的抓包调试也值得一提。微信开发者工具自带的Network面板能看到大部分请求但真机环境需要借助代理工具。我调试时用Charles做过抓包核心做法是给手机配置代理和证书然后在Charles里面看HTTPS请求。这条路径对排查小程序端“明明能请求但数据不对”的问题特别高效。需要注意的是抓包证书要在手机里信任否则看到的全是乱码而且微信开发者工具里如果开启了“校验合法域名”本机调试时也会发警告可以把不校验合法域名打开来缓解。最后再分享一个我个人很受用的经验做微服务both端的项目千万不要每一步都去追求最优解。先把一条完整的业务链路跑通比如“学员在小程序查看精品课程列表 → 点击详情 → 开始学习 → 记录学习进度 → 课程服务统计学习人数”然后把这条链路相关的服务和接口逐一亮出来再去做聊天、问答等附加功能。这个系统从立项到能稳定演示我实际只花了两周工作日的晚上其实大部分时间都花在调试服务间调用和部署环境上。很多人觉得微服务项目难多半是被“微服务”这三个字吓住了等你把业务闭环和基础设施盘顺了就会发现它真的没有想象中复杂。