
做了几年企业级应用见过不少把班级事务管理系统做成单体千层饼的案例——用户模块、事务模块、文件服务全塞在一个SpringBoot应用里刚开始还好等班级数上来、班委权限复杂化、移动端和PC端同时对接就发现每次改一个通知逻辑要重新构建整个工程测试成本直线上升。后来我重新梳理用SpringBootSpringCloudVue微信小程序把这套系统拆成了微服务架构整个过程踩了不少坑也总结出一套可以直接复用的落地经验今天完整地分享出来。这篇内容适合已经在学SpringBoot、想进阶SpringCloud微服务的同学也适合正在做班级管理、班委协同类毕设或实际项目的朋友。我会从为什么要拆分、服务怎么划分、双端怎么对接、线上遇到什么问题四个维度串起来代码和配置直接给到能跑的版本。1. 班级事务系统为什么值得上微服务1.1 需求梳理班委管理的真实痛点先盘点业务。一个典型的班级事务管理系统面对的角色至少有四种辅导员、班长、各职能班委学习委员、生活委员、团支书等、普通同学。每个角色对数据的要求完全不一样班长要发布任务、跟踪到人生活委员要管班费、宿舍检查记录学习委员要发课程通知、收作业普通同学只关心自己有没有被、有没有待办。这些需求看似简单但权限粒度非常细。比如班长能看全班事务学委只能看学习相关事务同学只能看关联自己的事务如果写在一个单体里Service层会越来越臃肿一个AffairService里塞了发布、审批、提醒、统计、权限过滤五类职责改一处影响一大片。更关键的是扩展压力。校园场景里经常有活动投票、问卷调查这种临时性高并发模块活动当天几百人同时点提交如果和基础事务模块混在一个进程里一旦内存或线程池被打满连登录都卡。把这类独立业务拆出去单独部署、单独扩容才能互不拖累。1.2 微服务拆分不能拍脑袋从业务边界和团队协作看微服务不是越碎越好。网上不少人一上来就拆七八个服务结果一个班级管理系统事务表就几千行搞了十几个服务运维直接崩溃。我个人的拆分判断标准有三个。第一个是业务边界是否清晰。用户身份认证是一类班级事务流是另一类文件存储是基础设施活动投票是临时热点。这四类彼此之间没有强事务依赖拆开后通过接口通信即可不涉及分布式事务这是可以拆的前提。第二个是变更频率是否差异大。认证逻辑很少改事务流程经常改文件存储几乎不改。把它们拆开改事务时不用重新部署其他服务发布风险小很多。第三个是是否有独立扩缩容诉求。上面说了活动投票瞬时流量大独立部署后可以给它单独配更高的实例数而基础服务保持低配省钱。按这个标准我最终把系统拆成五个服务auth-service认证、user-service用户与班级组织、affair-service班级事务核心、activity-service投票活动、file-service文件处理。另外加了一个gateway-service做统一入口。1.3 单体到微服务的迁移策略如果你现在手里已经有一套单体代码不建议一次性推倒重来。我当时的做法是先抽认证、再抽文件、最后抽业务。因为认证是所有请求的前置依赖文件服务和其他业务耦合最浅这两块相对好拆等基础设施拆完了再根据事务流程的复杂度拆业务。每次拆完一个服务跑一遍全量接口回归确认行为没变化再拆下一个。过程中发现真正难的不是代码本身而是数据库连接和事务边界的重新梳理。比如原来单体里一个方法里同时操作了用户表和事务表拆分后这两个操作变成了跨服务调用就不能再用本地事务保证一致性了。我的处理方式是核心数据操作控制在同一个服务内完成跨服务只做查询聚合不做联合写入。宁可多几次Feign调用也不引入Saga这种复杂方案毕竟班级事务场景对最终一致性的容忍度比电商订单高得多。2. 技术选型与整体架构设计2.1 后端底座SpringBoot SpringCloud组件怎么搭配版本选择是我第一个要强调的。SpringBoot 3.x 出来之后很多人直接上最新版结果发现javax.*变成了jakarta.*连spring-cloud的对应版本都对不上排查半天浪费时间。我这边生产验证过的稳定组合是SpringBoot 2.7.x SpringCloud 2021.0.x SpringCloud Alibaba 2021.0.5.0 Nacos 2.2.x。这套组合兼容性好网上资料也全遇到问题好查。核心组件选型如下组件作用选型理由Nacos注册中心 配置中心比Eureka多了配置管理控制台好用SpringCloud Gateway统一网关路由转发、鉴权过滤、跨域处理一站搞定OpenFeign服务间调用声明式HTTP客户端接口清爽Sentinel流量防护活动场景防刷、限流MyBatis-PlusORM单表CRUD零SQL分页好用Redis缓存 Token存储认证信息、热点配置缓存这里有人会问班级管理系统用得上Sentinel吗我的看法是活动投票接口确实需要。举个实际例子发布一个班级投票时间是晚上8点到8点半同学们同时涌进来瞬时QPS可能冲到几百。没有限流的话数据库连接池先被打满连带着认证接口一起超时。给投票接口配一个QPS200的流控规则超出的直接返回人多请稍后再试虽然丢了一些请求但保证了系统整体可用这不就是Sentinel的价值么。2.2 前端双轨制Vue管理端 微信小程序学生端这套系统的前端分两条线一开始我就决定不共用一套代码。原因是使用场景差异太大辅导员和班委在电脑上做事务管理需要表格、筛选、批量操作同学几乎只在手机上收通知、提交事务。混在一个Web应用里适配成本反而更高。管理端用Vue 3 Vite Element PlusVite的冷启动和热更新比Webpack舒服太多Element Plus的表单、弹窗、表格组件覆盖了九成后台场景。移动端用微信小程序原生语法 Vant Weapp。没有用uni-app因为这套系统里没有安卓App、iOS App等其他端的需求原生小程序性能最好、调试最直接。双端共用的核心是一套后端接口。区别只在于管理端走token放在Authorization头的标准方式小程序端因为登录链路不同微信code换sessionToken的获取方式和刷新逻辑要单独处理。后面第3节我会把两端的认证链路完整写出来。2.3 存储与中间件MySQL Redis MinIOMySQL负责所有业务数据库名就叫class_manager。核心表包括班级表、用户表、事务表、事务流程记录表、活动表、文件表表结构在第3节展开。Redis干两件事一是存登录Token对应的用户信息key设计为login:token:{token}过期时间设为2小时这样用户改密码或管理员踢人时可以直接删key比无状态JWT更可控二是缓存班级配置类和热点统计类数据比如班委名单、班级人数这些数据读多写少缓存命中率极高。MinIO是单独加的用来存文件。班级事务里审批附件、活动照片、作业文档这些都要落盘。用本地文件系统的方案我也试过问题很多一是管理端和小程序端多实例部署时文件不共享二是备份困难三是没有对象存储自带的分片上传和断点续传能力。MinIO部署简单兼容S3协议单机版一条命令就能跑起来做中小规模系统足够用。2.4 整体架构与一次完整请求的调用链架构布局用文字描述一下所有外部请求先到nginx按路径前缀分发到网关和生产环境的管理端静态资源网关做路由匹配和Token校验命中/auth/**的请求直接转发到认证服务命中/api/**的请求在网关校验Token后转发到对应业务服务业务服务之间通过OpenFeign同步调用所有服务注册到Nacos配置也统一从Nacos拉取本地不留敏感配置。说一次完整请求链路你就明白了班长在管理端点发布事务浏览器把请求发到https://域名/api/affair/create网关按路由规则把请求转发给affair-serviceaffair-service先调user-service的Feign接口确认当前用户是班长且班级ID正确然后写事务表和流程记录表再通过微信小程序订阅消息接口推送通知给相关同学整个过程返回一个事务ID前端拿到后刷新列表。学生端同学收到订阅消息点开小程序POST /api/affair/submit同样走网关进affair-service事务状态变更流程记录表追加一条已提交如果事务需要辅导员审批再触发审批节点。整条链路清晰每个服务职责单一出了问题看日志定位也快。3. 核心模块实现从数据库到接口的完整落地3.1 服务拆完后数据库怎么设计很多微服务项目死在数据库设计上因为每个服务虽然是独立进程但如果共用一个大库表的归属边界不清晰最后还是会退化回单体。我的原则是物理隔离逻辑清晰每个服务独占自己的库或一组表服务间不允许直接访问对方表只能通过接口。user-service负责用户和班级组织核心表有class_info班级表字段包括班级名称、辅导员ID、班长ID、入学年份、状态sys_user用户表字段包括微信openid、学号、姓名、角色编码、所属班级IDclass_member_rel班级成员关系表记录学生和班级的关联及在该班内的职务这是为了防止直接改sys_user造成历史数据混乱换班或转专业时只动关系表affair-service负责事务流核心表有affair_type事务类型表比如课程通知、任务布置、考勤记录、意见收集affair_info事务主表包括标题、内容、类型ID、发布人、班级ID、紧急程度、截止时间、当前状态affair_flow_record事务流程记录表每经过一个角色就插一条流水相当于轻量版的审批流affair_recipient事务接收人表记录哪些人需要处理这个事务、是否已读、是否已提交activity-service和file-service的表相对简单一个是投票活动和投票明细一个是文件元信息表文件名、MinIO桶名、对象名、大小、上传人、关联业务ID。表之间的关联全部用逻辑外键不建物理外键。微服务下分库了物理外键也建不了反而是逻辑外键配合代码里的事务检查更灵活。3.2 双端登录认证微信小程序和Vue管理端怎么共用一套Token登录是双端系统第一个要打通的地方。微信小程序端不能用账号密码方式太反人类必须走微信登录管理端则是标准的账号密码登录。但两个入口最后要落到同一套Token体系后面的业务接口才能一视同仁。小程序端流程前端调用wx.login()拿到临时code把这个code传给auth-service的/auth/mini/login接口。后端拿着code去请求微信的jscode2session接口换openid然后查sys_user表里有没有这个openid没有就自动创建用户并绑定默认角色同学有就正常登录。登录成功后生成一个自定义Token用UUID或JWT都行我用的是UUID因为后续要配合Redis做主动失效存Redis返回给小程序端。PostMapping(/mini/login) public Result login(RequestBody MiniLoginRequest req) { // 1. 调用微信接口换openid WxSessionResponse session wxService.code2Session(req.getCode()); // 2. 查用户表不存在则自动注册 User user userService.findByOpenid(session.getOpenid()); if (user null) { user userService.createByOpenid(session.getOpenid()); } // 3. 生成token并缓存到redis String token UUID.randomUUID().toString().replace(-, ); redisTemplate.opsForValue().set( login:token: token, JSON.toJSONString(user), 2, TimeUnit.HOURS ); return Result.ok(new LoginResp(token, user)); }管理端流程更常规账号密码提交到/auth/admin/login校验通过后同样生成Token存Redis。注意管理端用户角色校验比小程序端严格登录后必须在网关层判断角色编码不是管理角色的一律拦截。3.3 班级事务核心流程发布、接收、提交、审批事务是整套系统最核心的模块我把它设计成了一条状态机。发布时状态为进行中到截止时间自动切到已截止提交后如果允许撤回则状态回到进行中已截止事务管理员可以手动标记已办结。状态流转全部写在affair-service里用枚举做状态常量避免魔法值。发布事务的接口要考虑权限。班长、学习委员、团支书等角色都有发布权但只能发到本班级。我在user-service里维护了一个当前用户ID 班级ID 职务编码的复合查询接口事务服务通过Feign调用它完成权限校验自己不过度依赖用户表数据。事务创建之后两个动作几乎同时发生一是往affair_recipient表插入所有接收人记录二是推送通知。通知这块我分了两种手段管理端用站内消息小程序端用微信订阅消息。订阅消息需要提前让用户确认授权不授权就收不到这是微信平台规则决定的我一般会引导用户在首次使用时点击允许接收班级事务通知。批量插入接收人记录这步看着简单其实有性能坑。我之前用循环一条条insert班级60人就是60次数据库往返慢了近10倍。后来改成MyBatis-Plus的批量插入功能一次性提交时间从2秒降到200毫秒。微服务下的数据库性能优化很多时候不是加索引而是减少往返次数。3.4 文件服务MinIO接入和M3U8视频播放的方案文件服务拆成独立服务后接入方式很标准。前端先把文件传给file-service的上传接口后端生成一个新的对象名用UUID防止文件名冲突调用MinIO客户端把流写入桶成功后把文件元信息存表返回文件的访问URL给前端前端再把这个URL连同业务ID传给事务创建接口。PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) { String originalName file.getOriginalFilename(); String objectName UUID.randomUUID().toString() . StringUtils.substringAfterLast(originalName, .); // 上传到miniobucket按业务类型区分 minioClient.putObject( PutObjectArgs.builder() .bucket(affair-files) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); // 存元信息表 FileMeta meta new FileMeta(); meta.setFileName(originalName); meta.setObjectName(objectName); meta.setSize(file.getSize()); meta.setUploadUser(getCurrentUserId()); fileMetaMapper.insert(meta); // 返回可直接访问的url String url fileServiceBaseUrl /file/download/ objectName; return Result.ok(url); }很多人会问事务管理为什么要支持M3U8视频。实际场景是班级风采展示或者活动记录视频如果直接拿MP4在微信小程序里播放一是加载慢二是流量消耗大。M3U8切片后支持video标签直接播放也支持倍速和进度拖动体验好很多。我在文件服务里加了一个转码接口用FFmpeg把上传的MP4转成HLS格式m3u8ts切片然后返回m3u8地址。前端用video.js播放m3u8只需要引入HLS插件即可不需要额外安装任何本地组件。注意转码是异步任务文件上传接口先返回转码中状态前端轮询转码结果再刷新播放地址。4. 前端与小程序双端实现要点4.1 Vue管理端路由、权限拦截和列表性能管理端我用的是Vue 3的组合式API。路由设计采用动态路由方案登录后拉取当前用户的角色权限前端根据权限过滤出一份路由表通过router.addRoute动态注册。这样做的直接好处是同学角色即使手动改URL访问管理页面侧边栏和路由里压根没有这个页面。权限拦截的核心代码在axios拦截器里。每个请求带上Authorization: Bearer {token}响应如果返回401就清掉本地token并跳回登录页返回403说明权限不足弹个提示。这里有一处容易踩的坑token过期后不要立刻跳登录页先尝试调一次刷新接口刷新成功就自动重发原请求体验好得多。列表性能方面Element Plus的el-table在数据量超过500行时会有明显卡顿。我优化了三件事一是后端分页必须做对前端每次只拉20条二是表格列的插槽别写复杂嵌套组件尤其是状态标签和时间格式化能用计算属性提前处理好就别在模板里重复调用方法三是操作列的按钮不要给每一行都渲染完整的el-dropdown一个操作列最多三个入口更多操作折叠进更多里。4.2 微信小程序端登录、列表加载和顶部标题小程序端的页面我按角色分了几个Tab首页通知和待办、事务列表、我的。首页的核心是列表加载更多功能。微信小程序的scroll-view或onReachBottom触底加载逻辑不复杂但有几个细节值得注意下拉刷新要调wx.stopPullDownRefresh触底加载要防重入用一个loading布尔值控制分页游标建议用lastId而非offset因为数据量大了之后offset翻页深了性能会下降。Page({ data: { list: [], loading: false, finished: false, lastId: 0, pageSize: 10 }, onReachBottom() { if (this.data.loading || this.data.finished) return; this.loadMore(); }, async loadMore() { this.setData({ loading: true }); const res await wx.request({ url: ${BASE_URL}/api/affair/list, method: POST, data: { lastId: this.data.lastId, pageSize: this.data.pageSize }, header: { Authorization: Bearer ${wx.getStorageSync(token)} } }); const newList res.data.rows; this.setData({ list: this.data.list.concat(newList), lastId: newList.length ? newList[newList.length - 1].id : this.data.lastId, loading: false, finished: newList.length this.data.pageSize }); } })小程序顶部导航栏的标题也有讲究。原生导航栏的标题是在app.json或页面json里写死的但班级事务系统里不同角色看到首页标题不同——班长是班级事务管理普通同学是班级通知。我通过wx.setNavigationBarTitle在页面onLoad里动态设置标题配合后端返回的角色信息做切换。这个功能看似小但交互体验提升很明显用户不用自己判断应该去哪个入口。4.3 前后端联调从跨域到代理的完整配置联调阶段最浪费时间的就是跨域和路径不匹配。开发环境我的做法是Vue管理端在vite.config.js里配代理让/api前缀转发到http://localhost:8080网关地址小程序端因为不能走Vite代理直接在wx.request的url里写局域网的http://192.168.x.x:8080。// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: /api } } } } })小程序端的开发姿势是真机预览时必须把BASE_URL改成电脑的局域网IP同时要在小程序后台配置request合法域名开发阶段可以勾选不校验合法域名。这里有个坑提醒一下——微信开发者工具的不校验合法域名选项只在当前工具里有效真机预览时如果打开的调试模式被关掉请求就会失败。我的处理方式是在代码里留一个isDev开关开发环境默认走http://局域网IP:8080发布前统一替换成HTTPS正式域名。网关层的跨域配置也不要忘。虽然Vite代理解决了开发环境的跨域但网关本身需要支持小程序真机和以后可能的H5端直接访问所以要在Gateway里统一加CORS过滤器。配置时注意allowedOriginPatterns要学会用通配符别写死具体域名否则后续加域名又要改代码。spring: cloud: gateway: globalcors: cors-configurations: [/**]: allowedOriginPatterns: * allowedMethods: * allowedHeaders: * allowCredentials: true5. 常见问题与排查实录5.1 版本地狱SpringBoot版本太高引发的兼容性问题这是用SpringCloud最容易翻车的地方。我在一个分支项目里试过SpringBoot 3.2SpringCloud 2023结果项目能启动但Nacos注册不上原因是Nacos客户端对jakarta命名空间的支持版本不匹配。排查的时候看控制台日志全都是ClassNotFound根本不知道是哪一层的问题。解决思路是锁定经过验证的版本组合。上方表格列的组合我测过稳定可靠。如果你确实要用SpringBoot 3.x那你必须确认SpringCloud Alibaba版本在2023.0.1.0以上且Nacos客户端升级到2.3.x。结论是不要为追求新版本而升级除非你有明确的新特性需求。骨架项目锁定版本后在父POM里统一维护子服务全部继承杜绝各服务自己引版本导致冲突。5.2 网关路由404Feign和网关接口路径不一致我遇到过一个很诡异的问题管理端请求某个事务接口网关返回404但直接访问服务地址是通的。查了半天发现是服务之间Feign接口的context-path和网关路由里的前缀对不上。user-service配置里配了server.servlet.context-path: /user管理端调用affair-service没问题但affair-service通过Feign调用user-service时Feign默认会带上user-service的注册名作为前缀导致路径变成/user-service/user/info而服务端实际路径是/user/info。解决方式FeignClient注解里显式写path /user注册到Nacos的名字保持为user-service这样Feign拼接出来的路径就是/user/user/info——慢着还是不对。正确做法是统一约定注册名带-service后缀用于路由服务端context-path一律留空所有内部路径不带服务名前缀Feign接口里用path属性写服务的context-path。这次调整之后Feign路径和网关路由路径彻底分清楚了。5.3 小程序登录态失效code重复使用的坑调试中发现一个间歇性bug用户连续打开小程序几次之后偶尔会突然弹出登录失败请重新打开。查日志发现是code2session接口返回了invalid code错误。原因在于微信的wx.login()返回的code有5分钟有效期且只能用一次我的前端代码里在多个页面生命周期里重复调用了同一个code去换token第二次调用必然失败。修复方式很粗暴也有效把登录逻辑收敛到app.js的onLaunch里执行一次拿到token后存globalData所有页面直接读不再各自调登录。同时加一个401兜底任何接口返回401且代码里存着旧Token时才重新触发wx.login()换新登录态。5.4 文件上传大小限制与MinIO桶权限上传大文件时会遇到413 Request Entity Too Large或后端直接报MaxUploadSizeExceededException。有三个地方要一起改SpringBoot的spring.servlet.multipart.max-file-size和max-request-size网关的请求体限制默认可能只放行1MB以及nginx的client_max_body_size。三处漏一个都会失败我一开始只改了SpringBoot配置ngnix那层没动上传超过1M的作业附件还是会报错。MinIO桶权限也有讲究创建桶的时候默认是私有的直接访问文件URL会返回AccessDenied。我的方案是存储的桶设为私有对外提供文件服务的下载接口下载时通过MinIO SDK生成临时预签名URL设置有效期比如10分钟。这样既保证文件不被公开抓取又能正常预览下载。班级活动照片如果有分享诉求再单独给特定目录生成永久分享链接而不是整个桶公开。5.5 事务列表加载慢跨服务联表查询聚合的优化事务列表页需要显示发布人头像、班级名称、接收人已读数量这些数据分布在user-service和affair-service两个服务里。最开始我在循环里调Feign查用户信息接口一次返回20条事务就要调20次用户服务页面加载慢不说还容易把下游服务调用打满。优化方案是批量聚合affair-service先查出当前页的20条记录收集所有涉及的用户ID集合去重后可能就10个ID一次Feign调用user-service的/user/batch-info?ids1,2,3...接口拿到MapuserId, UserInfo在内存里完成组装。这样整个列表页的跨服务调用从20次降为1次响应时间从2秒降到300毫秒。提示微服务场景下任何涉及List查询的接口都要考虑N1问题。批量接口是标配不要写单个实体查询接口让上游循环调。6. 实操心得与后续扩展方向整套系统从单体重构到微服务再补齐小程序端我前前后后花了大约三周业余时间。最大的感悟是微服务架构的价值不在于技术栈有多亮眼而在于它逼着我把业务边界想清楚。以前写单体Service层可以随便互相注入代码一团乱麻拆成服务之后每个服务的职责、接口、数据归属必须提前设计否则上线即灾难。关于这套系统的扩展方向我实测下来最值得做的是两件事。一是给affair-service加一个简单的定时任务扫描把超过截止时间的事务状态从进行中自动更新为已截止替代手动改状态这个通过Spring的Scheduled就能实现注意多实例部署时配合分布式锁我用的是Redis的setIfAbsent防止重复执行。二是把事务流程记录表的数据做成可视化统计比如按事务类型、周维度展示班级事务数量趋势班委可以做学期复盘。这些扩展都不需要改架构在现有服务上平滑增加接口就行。如果你正在准备做类似的系统或者刚接触微服务想做点实际的练手项目我建议你从这套班级事务系统开始。业务不复杂但完整覆盖了微服务架构的注册发现、网关路由、Feign调用、文件存储、双端适配这些核心场景学一遍下来比单纯看SpringCloud教程有用得多。最后提醒一句边做边记录踩坑日志这些才是你未来面试时最有说服力的谈资。