
这几年高校信息化项目里“评审管理”类系统越来越常见但大部分都停留在简单的增删改查阶段。真正把申报、评审、汇总、公示、大屏展示整个链路打通还要扛得住几十个专家同时在线操作的确实不多。这个“研究生教学成果评审管理系统”项目我之前带着团队完整做过一版技术栈正好就是标题里的这套组合——SpringBoot Vue Spring Cloud微服务分布式架构外加一块可视化大屏。我先把结论放在前面这套技术选型在评审类系统里不算最简方案但如果你的系统未来要扩展到课题申报、学科评估、奖项评审这类同类业务微服务架构能帮你省掉大量重复开发和搬迁成本。这篇文章我打算从项目整体设计、微服务拆分、核心业务模块的实操实现到分布式事务、分布式锁这些硬骨头再到部署上线阶段踩过的坑完整复盘一遍。不管你是刚接手类似项目的开发还是打算从单体重构到微服务都可以直接照着里面的思路落地。我会尽量少讲虚的多给能直接抄的配置、代码片段和排查套路。1. 项目整体设计与业务链路拆解1.1 评审业务的核心痛点在哪研究生教学成果评审这件事表面看就是“交材料、找专家打分、出结果”真正落地的时候却有一堆细节问题。首先是申报材料的多样性。一份成果可能包含论文扫描件、获奖证书、项目结题证明、教学改革报告每个文件还有不同的格式要求和大小上限。材料一多存储和预览就成了第一个坑——你不能让专家点开一个PDF等十秒也不能因为某个人传了个500MB的视频就把整个系统拖垮。第二个痛点是评审流程的权限隔离。不同学院只能看本学院的申报材料评审专家只能看分配给自己的那一批成果督导组要看全局但不能打分管理员要能随时终止或调整某一轮评审。权限模型一旦设计不清楚开发后期全是补丁。第三个痛点是评分规则。教学成果通常不是简单加总而是按“材料完整性、创新性、应用成效、推广价值”几个维度打分不同职称的专家权重还可能不一样。更复杂的情况是某些类别的评审需要去掉一个最高分和一个最低分再取平均——这个规则看着简单放到分布式环境下处理多名专家并发评分时一不小心就会算错。第四个痛点是结果公示的时效性。评审期间分管领导、学院负责人、申报人三方都盯着进度看需要一个实时更新的大屏把“当前申报总数、已评审数、平均分分布、异常状态”这些数据可视化。没有大屏管理方就得反复人工导出Excel效率极低。1.2 单体够用为什么我还要拆微服务说实话一个校级评审系统的并发量可能还不如一个小型电商网站的双十一峰值。全校同时在线操作的专家乐观估计也就两三百人。如果只看并发Spring Boot单体应用完全够用硬上微服务甚至会被人喷“过度设计”。但我的真实判断标准不是并发量而是两条业务边界的稳定性和未来系统的扩展形态。评审管理系统的业务边界其实非常清晰——用户认证与权限、申报材料管理、评审流程控制、评分汇总计算、大屏数据聚合五个模块天然就是五条独立业务线。更重要的是学校信息化建设里这类系统不会只做一届评审就完事。下一届可能会新增“优秀论文评审”学院可能要求独立的“内审子系统”学校层面的统一门户又要对接统一身份认证。一旦这些需求到来单体应用只能整体部署、整体升级改一个评分公式都要重新发布整个系统。微服务把边界切开之后互不影响的独立发布、独立扩容、独立复用就变成了默认能力。申报材料服务可以单独扛文件上传压力评分服务可以独立做高可用新增一个评审类型只需要在流程服务里加一套配置。这是单体架构做不到的。当然微服务引入了分布式事务、服务调用链路追踪、配置管理这些额外的复杂度。我的建议是如果系统仅限一届评审、固定单一场景、后续不打算扩展就老老实实用单体如果你预见到这会是学校信息系统群的一个常驻成员微服务是值得支付的成本。1.3 功能模块划分与权限模型设计我最终把系统拆成六个功能域门户与认证、申报管理、评审管理、评分引擎、可视化大屏、消息通知。权限模型沿用经典的RBAC但针对评审场景做了两级角色扩展。一级是平台角色系统管理员、学院管理员、评审专家、申报人、督导员。二级是业务角色某轮评审的项目管理员、某评审组的组长、某学科的分组专家。两级角色组合之后一个用户可以同时是“学院管理员的申报人”也可以是“教学成果奖评审的专家”互不干扰。权限控制上用了Spring Security OAuth2JWT做无状态Token配合Spring Cloud Gateway做统一鉴权入口。每个微服务内部再做一次资源的二次校验防止越权调用。比如评审专家调用“提交评分”接口时评分服务内部必须校验当前用户是否在该轮评审的专家名单里、该成果是否确实分配给了他、该轮评审是否处于“进行中”状态。三重校验一层都不能少这是评审类系统的底线。2. 微服务技术选型与服务拆分落地2.1 组件选型的完整清单与理由组件选型理由注册中心与配置中心Nacos同时解决服务注册发现和配置管理部署轻量界面友好国内开源社区活跃网关Spring Cloud Gateway基于WebFlux性能好路由断言灵活支持统一鉴权与限流服务间调用OpenFeign声明式HTTP客户端搭配Sentinel做熔断降级配合负载均衡熔断与限流Sentinel规则可动态推送Dashboard监控直观网关层和业务层都能接入认证授权Spring Security OAuth2 JWT生态成熟配合Gateway做统一Token校验分布式事务Seata AT模式评审业务的一致性要求高AT模式无侵入适合非极端高并发场景分布式锁Redis Redisson处理重复评分、重复提交等并发问题自带看门狗机制避免死锁文件存储MinIO兼容S3协议内部部署免费权限控制粒度细支持预签名URL前端Vue3 Element Plus EChartsVue生态成熟Element Plus覆盖管理端后台ECharts满足大屏图表选Sentinel而不是Hystrix主要原因是Spring Cloud Alibaba生态里Sentinel的维护更活跃而且它支持在控制台实时修改限流规则不用改代码重启服务。评审期间专家可能集中在某几分钟内提交评分网关层加一个简单的并发限流就能挡住异常峰值避免下游服务被打挂。2.2 六个核心微服务的边界与职责我按“业务能力”而不是“数据表”来切分服务保证每个服务有独立的数据库和清晰的对外接口。认证服务auth-service负责登录、Token颁发与刷新、用户查询、角色权限校验。所有服务内部的权限二次校验都通过Feign调用它的接口完成。这个服务不碰任何评审业务数据。申报服务apply-service负责申报人提交成果、上传附件、修改申报信息、查看审核状态。文件上传走MinIO预签名URL元数据存MySQL。这个服务是整个系统的数据入口也是文件流量最大的服务。评审服务review-service核心业务服务管理评审轮次、专家分组、成果分配、评分提交、评分规则配置。评分提交接口是整个系统中并发压力最大的地方。流程服务process-service负责评审各阶段的状态机流转。待提交、待初审、专家评审中、汇总计算、结果确认、公示中、已结束一共七个状态。状态流转通过消息驱动和状态机双重保障。大屏服务screen-service独立的数据聚合服务对接大屏展示所需的数据接口内部做缓存、聚合和实时统计。这个服务不参与业务写入只读因此可以单独部署多个实例提升吞吐。通知服务notice-service对接站内信、企业微信和邮件负责把评审进度、待办提醒、结果通知推送给相应角色。同样通过消息队列解耦。2.3 服务拆分时最容易踩的边界坑我见过不少团队拆微服务拆完发现比单体还难维护原因是他们把“接口”当成了“服务”。比如把一个申报提交接口拆成“保存基本信息服务”“保存附件服务”“保存推荐意见服务”结果一次提交要调用三次接口任一环节出错数据就对不上。正确的拆法是按业务完整闭环来拆——申报提交就是一个完整的业务能力内部包含基本信息校验、附件上传、状态更新、消息通知全部封装在申报服务内部完成。另一个坑是数据库权限。拆服务之后A服务直接连B服务的数据库是绝对红线。我们定了一条铁律任何跨服务的数据读取必须通过对方提供的接口不能直连数据库。刚开始团队觉得麻烦后面出现数据权限问题的时候才明白这条铁律的价值——至少你不用排查是不是有别的服务悄悄改了你库里的数据。2.4 前端Vue工程与可视化大屏的架构落地前端我拆成了两个独立工程管理后台用Vue3 Element Plus面向申报人、专家和管理员可视化大屏单独用一套Vue3项目组件全部围绕ECharts和DataV定制。管理后台的页面布局比较典型左侧菜单、顶部用户信息、右侧内容区。动态路由按角色渲染管理员登录后能看到评审配置菜单专家登录后只能看到“我的评审任务”和“历史评审记录”。路由权限用Vue Router的addRoute动态添加刷新页面时再从后端拉取一次权限数据重新生成路由。大屏项目单独处理的理由很简单——它的渲染逻辑和后台差异太大。大屏需要1440x900甚至更高分辨率下的自适应需要数据实时刷新需要弱化交互强调展示效果。我用的方案是外层套一个scale容器内部按1920x1080设计稿固定尺寸布局通过动态计算视口宽高比做整体缩放保证不同屏幕上元素不变形。这个方案比一个个rem换算省事得多实测在普通21:9显示屏和16:9投影仪上都正常。数据刷新上大屏服务提供聚合接口前端每10秒轮询一次。有人可能觉得应该用WebSocket推送但评审大屏的数据变化频率是分钟级的轮询足够还能省掉WebSocket断线重连的一堆麻烦。实时性要求更高的“当前正在评审人数”指标我用了一个轻量级方案评审服务评分提交成功后向Redis写入一条带过期时间的计数大屏接口直接读Redis值延迟不超过1秒。3. 核心业务模块的实操实现细节3.1 工程初始化的版本选择与目录结构Spring Boot版本我用了2.7.x系列Spring Cloud用的2021.0.xSpring Cloud Alibaba用的2021.0.5.0。这三个版本组合实测兼容稳定网上资料也多遇到问题基本能在三天内查到解决方案。不建议在没有老项目兼容要求的情况下一味追最新版本Spring Cloud Alibaba和Spring Boot之间经常存在版本对齐问题新版本刚出的时候坑特别多。工程结构上我用的Maven多模块方式evaluation-system/ ├── auth-service/ ├── apply-service/ ├── review-service/ ├── process-service/ ├── screen-service/ ├── notice-service/ ├── common/ │ ├── common-core/ // 公共工具类与实体基类 │ ├── common-redis/ // Redis与Redisson配置 │ ├── common-security/ // JWT解析与安全注解 │ └── common-feign/ // Feign统一配置与错误解码器 └── gateway-service/公共模块只放真正被公共复用的代码千万不能把一个服务里的业务工具类随手丢进common里否则common会变成一个垃圾堆。我管这个叫“公共模块的公共卫生问题”版本一变所有服务都要跟着重新打包发布。3.2 申报提交链路的核心接口实现申报提交这个动作看起来只是“填表传文件”实际上牵扯到一条很长的数据链路。我把它拆成几个顺序步骤第一步前端调用MinIO预签名接口获取文件上传地址文件直接传到MinIO不经过应用服务器。这一步很关键能避免文件流占用应用服务器的带宽和内存。MinIO的预签名URL我用的是Bucket策略加自定义前缀隔离每个申报人只能上传自己名下的材料路径规则是/apply/{userId}/{applyId}/{fileName}。第二步文件上传完成后前端携带文件元数据文件ID、名称、大小、类型、MinIO路径和后端基本信息一起提交。第三步申报服务接收请求后先做一轮完整的数据校验然后写入申报主表、成果明细表、附件信息表最后通过RocketMQ发送一条“申报已提交”的消息。通知服务消费消息后给学院管理员推送待办提醒。这里有个重要细节第二步到第三步之间的文件元数据必须做防篡改校验。文件上传完成后前端拿到MinIO返回的文件ETag再把它提交给后端后端调用MinIO的StatObject接口核对文件是否存在且大小一致。不这样做的话用户很可能传一个空文件或者假路径后面专家评审时点开一片空白。3.3 专家评分接口与服务端二次校验专家评分是整个系统里唯一“高频写入”的接口。设计时我做了三层防护。第一层是网关层限流Sentinel对/review-service/api/score/submit配置了QPS限流单机不超过50超过直接返回“系统繁忙请稍后重试”。第二层是业务参数校验。评分提交参数包含评审轮次ID、成果ID、各维度分数、评语。后端首先校验分数范围每个维度0-100再计算是否存在重复提交。第三层是分布式锁防重复。一个专家对同一份成果的重复提交可能有三种来源前端按钮狂点、网络重试、不同设备同时操作。只用数据库唯一索引防重的话要在业务代码里捕获异常再翻译成友好提示比较丑。我选择用Redission的RLock加权锁以review:score:{roundId}:{resultId}:{expertId}为锁粒。String lockKey review:score: roundId : resultId : expertId; RLock lock redissonClient.getLock(lockKey); boolean locked false; try { // waitTime 1秒leaseTime 10秒由看门狗自动续期 locked lock.tryLock(1, 10, TimeUnit.SECONDS); if (!locked) { return R.error(正在处理中请勿重复提交); } // 二次查询确认尚未评分 ScoreRecord record scoreRecordMapper.selectByRoundAndResult(roundId, resultId); if (record ! null) { return R.error(您已对该成果评分请勿重复提交); } // 执行业务写入 scoreRecordMapper.insert(...); } finally { if (locked lock.isHeldByCurrentThread()) { lock.unlock(); } }这里特别说一下Redisson的看门狗机制它是解决锁超时误判的重要保障。默认情况下如果拿到锁之后业务执行超过leaseTime锁会被自动释放结果就是两个线程同时进入业务代码数据库就出现了重复数据。Redisson的看门狗会在锁存活时间剩余三分之一时自动续期只要业务没结束锁就不会过期。所以leaseTime参数可以直接不传或传一个保守值让看门狗兜底。3.4 评分聚合计算的规则引擎设计评分提交之后系统需要对同一个成果的多位专家评分做聚合计算。我的规则引擎支持三种算法直接平均、去除最高最低后平均、加权平均。不同评审轮次可以配置不同的算法同一轮次内不同类目也可以不同。聚合任务由RocketMQ延迟消息触发。所有专家评分完成后评审服务向MQ发送一条延迟消息两分钟后消费此时正常情况下所有评分都已入库。消费逻辑里再校验一次是否满足“已评分专家数等于应评专家数”满足则执行聚合不满足则重新发送延迟消息等待下一次。聚合计算的Java实现里有两个细节坑。第一个是“去除极值后平均”需要保证至少5位专家评分否则去掉最高最低后样本太少结果失真。我在规则配置里做了最小专家数限制。第二个是浮点精度问题。平均分需要保留两位小数用BigDecimal计算而不是double否则59.99分和60.00分的边界会出问题。所有分数存数据库时统一用DECIMAL(5,2)计算时全部转BigDecimal并设置RoundingMode.HALF_UP。BigDecimal total BigDecimal.ZERO; ListBigDecimal scores ...; // 该成果的全部得分 BigDecimal max scores.stream().reduce(BigDecimal::max).get(); BigDecimal min scores.stream().reduce(BigDecimal::min).get(); ListBigDecimal valid scores.stream() .filter(s - s.compareTo(max) ! 0 s.compareTo(min) ! 0) .collect(Collectors.toList()); BigDecimal avg total.add(...).divide(BigDecimal.valueOf(valid.size()), 2, RoundingMode.HALF_UP);这里加一个提示去掉最高最低后平均的算法一定要先确认合法评分人数。我遇到过某个成果实际评分专家只有4人按规则去掉2个极值后只剩2个样本最后算出来的分数明显偏离实际情况。后面我在配置里直接限制如果有效样本数低于设计阈值就降级为直接平均并给出提示。4. 分布式场景下的三大硬骨头事务、锁与数据一致性4.1 分布式事务Seata AT模式还是本地消息表评审业务里典型的跨服务事务场景是评审确认结果时要同时更新评审服务里的成果状态、流程服务里的评审阶段状态、通知服务里的消息记录。这三个操作分布在三个服务任何一个失败都会导致数据不一致。我最终选择了Seata AT模式理由是业务并发量不高AT模式的全局锁影响可以忽略而且它对业务代码无侵入只需要在全局调用方加GlobalTransactional注解。Seata的AT模式通过数据源代理自动记录UNDO_LOG发生异常时反向补偿已经执行的SQL操作对团队来说学习成本最低。seata: enabled: true application-id: review-service tx-service-group: evaluation_tx_group service: vgroup-mapping: evaluation_tx_group: default grouplist: default: seata-server:8091实际使用中配置项不多但要注意Seata的AT模式对数据库的隔离级别有要求事务中涉及的表必须有主键且不能有复合主键否则UNDO_LOG的回滚数据构建会出问题。另外参与分布式事务的服务必须全部使用Seata代理过的数据源如果一个服务漏配整个事务链路的回滚就断了。对于“申报提交”场景我用的是本地消息表方案而不是Seata因为文件上传和申报入库之间天然存在“先成功后失败”的边界用本地消息表更轻便。申报主库和消息表放同一个数据库业务操作和消息插入在同一个本地事务里完成异步任务扫描消息表发送通知发送成功后标记消息状态。这套方案业务侵入少、实现稳定处理跨服务的异步一致性问题很够用。4.2 分布式锁在评审系统中的四个使用场景评审系统里不止评分提交一个并发场景我总共遇到过四个需要分布式锁的地方重复评分——前面讲过用Redisson锁防止同一专家对同一成果的重复评分。申报编号生成——申报单号要求全局唯一且按序号递增多个申报人同时提交时用Redis的INCR命令加日期前缀生成单号天然线程安全不需要锁。评审轮次启动和终止——管理员点击“启动评审”时系统要初始化专家分组、批量生成待评审记录。点击的那一瞬间如果多个管理员同时操作会生成两套评审分配数据。这里我用Redis锁锁住review:round:{roundId}:op同一时刻只有一个人能执行状态变更。大屏热门数据的缓存重建——大屏接口有缓存缓存过期后有大量请求同时回源查数据库我用Redisson的tryLock作为分布式互斥锁只有拿到锁的请求重建缓存其余请求直接返回旧的过期数据或短暂等待避免缓存雪崩。分布式锁不是银弹使用时要时刻记住一个问题锁的粒度一定要细。我在大屏缓存重建场景里最开始把锁粒度设成了screen:cache:all结果大屏某个板块的缓存过期其他板块的刷新也全部堵塞大屏数据几分钟都不更新。改用screen:cache:{moduleKey}按板块分锁之后问题立刻消失。4.3 可视化大屏背后的数据一致性与性能权衡大屏数据不是直接从业务库实时查出来的中间隔了一层。我的数据链路是业务库 - 实时统计服务 - Redis缓存 - 大屏接口 - 前端图表。为什么中间要加一层而不是让大屏接口直接查业务表因为大屏需要的数据都是聚合统计比如“各学院申报数量分布”“专家评分进度”“评审通过率趋势”这些查询如果实时去业务库跑会大量占用数据库连接和CPU。评审业务本身就有不少写操作被大屏查询拖累了就很亏。所以我用了一个异步统计方案。业务服务在关键节点申报提交、评分提交、评审状态变更发生后向MQ发送一条统计事件统计服务消费事件并更新Redis里的聚合计数同时每五分钟做一次全量校正防止MQ消息丢失导致计数偏差。这里有个细节大屏展示的“当前评审中”状态和“已完成”状态可能来自两套不同的统计逻辑如果两套逻辑的统计口径没对齐就会出现已完成数加进行中数不等于总任务数的情况。我最后把所有统计指标都定义成统一口径从同一个事件类型驱动保证左上角趋势图、右下角汇总表中间的数据彼此对得上。前端ECharts的图表更新逻辑也很简单一个setInterval轮询大屏接口拿到新数据后更新option。只要后端接口数据量控制在100KB以内10秒一次的轮询对服务器压力很小对视觉效果也足够流畅。5. 环境搭建、项目部署与上线前后的排查清单5.1 从零搭建微服务环境的三个关键步骤这个项目我踩过环境搭建的很多坑总结下来三个最关键。第一个是Nacos的命名空间与分组规划。建议从第一天起就把环境配置列表分成“dev、test、prod”三个namespace每个namespace下按服务名建配置。千万别开发测试生产全共用一套配置否则调试限流规则的时候误改生产数据后果很严重。我在Nacos里设置了三级配置结构公共配置所有服务共享的数据源、Redis地址、服务配置各服务独有的端口、数据库、消息队列、动态规则Sentinel流控规则、开关配置。第二个是Feign调用的超时与重试机制。Feign默认超时时间很短评审服务内部调用认证服务校验权限时如果认证服务刚好在做GC就会触发Feign超时重试本来只是慢一点结果因为重试把负载又抬高了一截。我的处理方案是连接超时设3秒读取超时设5秒重试关闭——因为评审系统的调用方基本是从接口进入的网关层已经有重试策略了业务层再重试容易重复写入数据。第三个是网关层的全局异常处理。Gateway是基于WebFlux的异常处理和传统的Spring MVC完全不一样不能直接复用RestControllerAdvice。我写了一个GlobalErrorWebExceptionHandler实现ErrorWebExceptionHandler接口统一处理网关层的路由不存在、鉴权失败、限流触发三类异常返回统一格式的JSON。这个坑非常隐蔽不写网关的人基本不会碰到。5.2 上线后真实遇到的高频故障速查表问题现象直接原因解决方案专家名单导入后部分专家登录提示无权限导入时角色初始化走了异步消息通知服务消费失败后角色没有落库导入流程改为同步落库后再发异步消息消费失败做重试和补偿大屏显示“评审完成”数据比实际多统计事件在MQ乱序消费先处理了完成事件后处理提交事件统计接口改成幂等更新用Redis记录事件最后处理时间乱序事件跳过高峰期评审评分偶发一直转圈Sentinel默认在网关层限制了并发线程数规则设置过小调整规则为按QPS限流而非按线程数限流并给评审评分接口单独设置更大的阈值文件上传大文件时前端超时MinIO的预签名URL有效期过短用户操作慢导致URL过期上传链接有效期从10分钟改成30分钟并在前端对上传超时做明确提示多实例部署后定时任务重复执行多个服务实例同时执行同一套定时任务用ShedLock配合Redis做任务分布式锁只在单实例上执行评审结果更新后大屏趋势图不变大屏接口读取的是五分钟前的全量缓存增量事件未触达缓存更新将核心指标的缓存更新改成事件驱动全量校正只作为兜底这些故障没有一个是“玄学”全部能通过日志和链路追踪定位到根因。我给团队立的规矩是所有跨服务调用必须带traceId日志统一打到ELK遇到问题先查traceId再讨论其他。时间长了之后排查效率提升非常明显。5.3 正式开放前必须检查的五项配置评审系统一旦正式开放申报中途改配置是代价最高的事情。我列一个上线前检查清单每项都是我实际踩过坑之后的经验产物第一项数据库连接池与冗余配置要提前压测。默认的HikariCP连接池20个连接看着够用但评审高峰期专家同时查询申报材料可能瞬间打满。我把连接池下线检测间隔调到了2秒连接超时设为30秒并预留了5个闲置连接做突发缓存。第二项MinIO的资源配额和生产环境桶策略。评审附件类型限定了PDF、图片和压缩包单文件最大100MB每个申报人总附件不能超过500MB。配额在MinIO桶策略里直接限制后端不用每次都循环扫描文件列表做判断。第三项Redis内存和淘汰策略。我用的淘汰策略是allkeys-lru但给大屏缓存键加了固定前缀防止低价值的临时锁键占满内存后把大屏核心缓存挤掉。第四项消息队列的消费重试次数。通知服务消费失败时默认重试16次间隔时间从1秒指数递增到2分钟。这个参数必须显式配置否则默认值可能把一些瞬时故障当成永久故障白白消耗重试次数。第五项网关和服务的优雅停机。发布新版本时旧实例必须先把注册中心状态改成下线等待存量请求处理完毕再退出。我配置了Spring Boot的server.shutdowngraceful并在Nacos里设置每30秒一次心跳检查至少保证一次完整发布周期内无请求中断。5.4 评审验收大屏时的演示小技巧最后分享一个和评审系统本身关系不大但很实用的技巧。可视化大屏在正式验收或汇报时最怕现场网络抖动导致数据图表空白或者演示环境杀进程导致缓存全失。我养成了一个习惯大屏系统单独部署一台演示机本地保留一套静态JSON快照ECharts优先读接口接口失败后自动降级读取本地JSON文件。演示的时候即便后端接口完全不可用大屏依然能展示完整流畅的图表动画。这个降级方案成本极低但能在关键时刻保住项目的交付印象分。另外一个经验是在大屏界面放一个很小的“数据更新时间”文本。每次轮询成功就刷新这个时间演示时领导看到“刚刚更新”这四个字比任何口头解释都更有说服力。这也是从实际交付中总结出的处理细节很值得保留。