高校竞赛管理系统二次开发:SpringBoot+Vue+MyBatis实战

发布时间:2026/10/20 18:05:32
高校竞赛管理系统二次开发:SpringBoot+Vue+MyBatis实战
上个学期我接手了一个让人头大的需求帮某高校把分散在Excel、微信群、纸质报名表里的竞赛信息统一到一个在线系统里。多方对比之后我选择以一套SpringBootVueMyBatis架构、MySQL数据库支撑的高校竞赛管理系统源码为基底进行二次改造。这套系统覆盖了赛事发布、在线报名、赛程管理、成绩录入、奖项生成、证书导出这条完整链路对高校里负责竞赛的老师、学生干部、教务管理人员都很实用。如果你正打算从零开发或直接改造一套同类系统这篇文章里涉及的模块拆分、表结构设计、权限处理、部署要点都是我实际动手过程中的经验总结可以直接参考。1. 竞赛管理系统的业务解剖从报名到发奖的完整链路1.1 竞赛业务中的三类核心角色与流程高校里的竞赛业务看着只是“发布通知、收报名、排比赛、给成绩”实际跑起来才发现流程约束非常多。我接手时先梳理了真实场景下的角色清单而不是照搬管理系统的通用模板。系统里至少有三类核心角色学生、指导教师、竞赛管理员。学生需要看到所有可报名的赛事能组队报名能上传作品或论文能查询自己的成绩和获奖状态指导教师需要审核自己名下学生的报名信息可能还要录入指导作品的相关材料竞赛管理员承担的工作最杂——创建赛事、设置报名截止时间、安排初赛复赛、分配评委、录入成绩、按规则生成奖项。除这三类之外竞赛中经常出现“院系负责人”和“评委”这两种附加角色。院系负责人只看本院系学生报名情况方便统计参与率评委只关心自己负责的赛项和评分任务不能看到其他评委的打分。一套企业级竞赛系统如果连评委数据隔离都做不好后面会非常被动。流程上最容易被忽略的是“赛事状态的推进”。一个竞赛从草稿到已发布从报名中到报名截止从评审中到成绩公布不是简单的字段状态而是会影响整个页面权限和操作按钮的显示。我在这套源码里看到的设计是赛事表里用 status 字段控制状态机不同状态下管理员能执行的操作完全不一样。比如赛事处于“评审中”报名入口、作品修改入口都要自动关闭处于“已结束”则只能查看、导出不能编辑。1.2 功能模块的优先级划分哪些必须做哪些可以后置拿到源码第一件事不是看代码而是把需求按优先级排出来。竞赛管理系统如果一上来就追求大而全很容易拖垮开发周期。按我的实际经验第一优先级是赛事发布、在线报名、报名名单导出、成绩录入、奖项生成。这五个功能跑通了整个流程就能运转。第二优先级是作品在线提交、评委在线评分、证书自动生成、大赛数据统计。这些属于“锦上添花”但也是高校最常提的增量需求。第三优先级才考虑消息通知、公众号对接、赛事归档、参赛选手自主修改报名信息这类边缘功能。我在改造时发现的顺序问题很典型很多开发者一上来先做漂亮的前台页面和复杂的权限控制结果核心报名流程还没走通老师根本没法用。正确做法是先把数据库表和Core API打通再回头优化界面。这套源码的目录结构也是按这个思路排的——真正复杂的业务逻辑集中在竞赛模块、报名模块、成绩模块三个包下权限和系统管理反而在后端里只占很小一部分。1.3 一张数据表清单看懂全系统下面这张表是我整理这套源码时梳理出的核心表清单基本对应了系统的完整数据底座数据表职责说明关键字段competition竞赛基础信息name, category, level, start_time, end_time, statuscompetition_stage竞赛阶段/赛程安排competition_id, stage_name, stage_order, begin_time, finish_timeregistration报名主记录competition_id, team_name, captain_id, status, create_timeregistration_member报名成员明细registration_id, student_id, student_name, student_nowork_submission作品/材料提交registration_id, file_url, submit_time, versionjudge_task评委任务分配competition_id, judge_id, stage_id, assign_timescoring_record评分记录judge_task_id, criteria_id, score, comment, statusaward奖项结果competition_id, registration_id, award_level, award_namesys_user系统用户username, password, role_id, department_idsys_role角色定义role_name, role_code, data_scope这里我要特别解释一下 registration_member 和 work_submission 的设计。很多小型系统会把“团队成员”用逗号拼接成一个字段塞在报名表里这种设计做展示没问题但一旦涉及资格审核、按院系统计、导出证书就会非常痛苦。多成员用独立子表存储关联查询和统计都干净很多作品提交单独拆表也很有必要因为同一个报名记录在初赛、复赛阶段可能提交不同版本的材料用 version 字段做版本管理比覆盖旧文件稳妥得多。2. 后端架构设计SpringBootMyBatis如何切分模块2.1 为什么选SpringBoot单体而非微服务高校竞赛管理系统的用户量级通常不会超过万人并发的核心场景集中在报名截止前和成绩公布后这种体量如果用微服务架构反而是给自己找麻烦。SpringBoot单体应用在这类项目里有非常明显的优势开发迭代快、部署只需要一个JAR包、本地调试不需要启动一堆中间件。真正到了流量增长的阶段单体通过集群部署也能扛住没必要一上来就拆服务。我见过不少项目把简单业务强行拆成用户服务、竞赛服务、文件服务结果服务间调用链变长分布式事务变成噩梦最后还不如单体好维护。这套源码的选择很务实后端按业务模块拆包而不是按服务拆分。com.xxx.competition.controller、service、mapper之下再按模块建子包代码结构清晰又不牺牲单体的简单性。2.2 MyBatis多表关联与SQL组织经验MyBatis在这套系统里承担的是数据持久化层使用上最大的难点不是增删改查而是“多条件动态查询”和“多表关联后的分页”。竞赛管理后台里管理员需要按竞赛名称、赛事级别、举办时间、审核状态等多个条件组合筛选列表MyBatis的 where 标签配合 if 标签能很好解决动态SQL问题。举一个实际例子查询竞赛列表时不是简单查竞赛表还要统计每个竞赛的报名人数、当前阶段、审核状态。这时候我通常会写一个专门的查询对象 CompetitionQueryDTO然后通过多表 left join 一次性组装完整视图select idselectCompetitionPage resultTypecom.xxx.pojo.vo.CompetitionVO SELECT c.id, c.name, c.level, c.status, COUNT(DISTINCT r.id) AS registered_count FROM competition c LEFT JOIN registration r ON r.competition_id c.id WHERE 1 1 if testname ! null and name ! AND c.name LIKE CONCAT(%, #{name}, %) /if if teststatus ! null AND c.status #{status} /if GROUP BY c.id, c.name, c.level, c.status ORDER BY c.create_time DESC LIMIT #{offset}, #{pageSize} /select这里有几个坑需要提醒一是分组后无法直接使用 PageHelper 对原SQL分页因为分页插件会拼在 group by 之后结果有时不是预期效果。我改造时改用 LIMIT 手动分页反而更可控二是 left join 造成的行数膨胀会导致 COUNT(DISTINCT r.id) 额外开销所以在列表查询和详情查询中要分开写SQL不要一个万能查询走到底。2.3 核心表结构设计报名表、竞赛表、成绩表的关联关系刚说了整体表清单这里再拆开讲一下关联关系。竞赛表 competition 与阶段表 competition_stage 是一对多关系阶段表通过 stage_order 控制先后的展示顺序报名表 registration 通过 competition_id 关联竞赛通过 status 区分草稿、已提交、已通过、已驳回等状态成绩表在系统中其实并不单独存在而是由 scoring_record 按评分明细保存最后通过聚合生成奖项。成绩表设计最容易犯的错误是“只存最终分数”这样一旦评委输入错误无法追溯评分过程。我在系统里采用的方案是评分记录表保存每一项评分标准的分值、评语、评委ID、评分时间最终成绩通过视图或统计SQL实时聚合。这样不仅保留了中间过程还能随时生成详细的成绩单遇到学生对分数有异议时可以直接调出原始打分记录。竞赛与奖项的关联也比较特殊一个竞赛可以产生多类奖项奖项并不是“竞赛表里的一个字段”而是一张独立表。原因是不同赛项可能有不同奖项类别比如校级一等奖、二等奖、优秀奖还有最佳创意奖、最佳组织奖这类特殊奖项。award 表按 competition_id registration_id 关联award_level 字段区分奖项级别这样导出证书时也能按照奖项级别排序。3. 权限与安全设计学生、教师、管理员三种角色的坑3.1 基于RBAC的菜单权限与按钮权限大学生竞赛平台的角色权限说复杂不复杂但想做得“像企业级”就一定要考虑数据范围问题。普通的RBAC只解决“能不能进入某个菜单”但高校场景里还有一层隐藏要求院系管理者只能看到本院系的学生数据。这套源码的角色设计分了三个层面第一层是菜单权限通过 sys_role_menu 中间表控制系统登录后根据用户角色渲染不同的左侧菜单第二层是按钮权限比如管理员有“审核通过”按钮普通学生没有前端用 v-permission 自定义指令判断第三层是数据权限通过 department_id 实现院系管理员查询学生列表时强制带上本部门过滤条件。我看到很多失败案例都死在第三层。比如有开发者只在SQL里加了一个 if 判断传了部门ID就过滤没传就不过滤结果学生在接口请求里删掉部门参数就能看到全校数据。正确做法是在Service层强制读取当前登录人上下文而不是信任前端传入的参数后端的查询条件永远从会话上下文获取前端只能传业务筛选条件。3.2 报名接口的防重复提交与幂等控制竞赛报名和电商下单很类似学生手一抖连点两次提交就会产生两条报名记录。这套源码的防重复处理用了两层机制。第一层是前端的按钮 loading 状态点击提交后立即禁用按钮等接口返回后再恢复。第二层是后端的唯一约束registration 表加了 competition_id、team_name、create_time 的组合控制报名模块提交时再根据队长学生ID 竞赛ID去查重。这里我要提醒一个更隐蔽的问题报名不是单表插入而是“报名主表 团队成员表 可能存在的作品记录”一次性事务写入。如果接口没有加 Transactional主表插入成功但成员表插入失败系统会留下一条不完整的报名记录学生可能以为自己报名成功了实际上后台名单缺人。改造时我会在所有涉及多表写入的Service方法上显式声明事务并且在事务内捕获异常后抛出运行时异常让Spring执行回滚。幂等控制方面更严谨的做法是引入一个“报名单号”字段前端生成唯一标识后端通过该字段做唯一约束。我当时没有大规模引入Token机制因为学生端的真实并发量不大用数据库唯一索引做兜底已经足够。但如果未来要对接多个院系、多个渠道报名幂等键设计建议提前考虑。3.3 JWT登录态管理与接口防刷系统的登录模式用的是JWT Token后端登录成功后签发Token前端存储在本地存储中。这种方式和传统Session相比最大好处是后端不需要维护会话状态集群部署时不需要共享Session非常适合高校这种“一台服务器就够但偶尔要临时扩容”的场景。不过JWT有个容易忽略的坑Token一旦签发在有效期内无法主动作废。学生账号被删除后旧Token如果还没过期理论上仍然可以访问接口。我的处理办法是做一个简单的用户状态检查拦截器每次请求进入时除了校验JWT签名还会查一次用户表确认账号状态和角色是否变更。这个查询不需要走普通业务查询直接走Redis缓存内存型操作开销很小。接口防刷在报名系统中也很重要。我不建议对所有接口做统一频率限制因为页面静态资源、登录接口、报名提交接口的访问特征完全不同。当时采用的做法是报名提交接口使用自定义注解 RateLimit限制单个用户每分钟最多提交10次请求超过后返回提示登录接口做简单的失败次数锁定连续5次密码错误账号锁定15分钟。这样既不会影响正常操作也能挡住绝大多数恶意脚本。4. Vue前端实战后台管理与竞赛前台的双端拆分4.1 为什么不建议前台后台共用一套布局市面上很多竞赛管理系统为了省事直接让管理员和学生在同一套Vue布局里切换角色但我实际用下来体验很差。管理员需要的是密集的表格、筛选器、批量操作按钮学生需要的是清晰的赛事卡片、报名引导、倒计时提示这两种界面形态完全不一样。这套源码在改造后拆成两套Vue应用一套是面向管理员和教师的管理后台基于经典的路由懒加载布局侧边栏导航另一套是面向学生的竞赛前台走移动优先的卡片式布局重点突出赛事状态。两套应用共用同一套后端API但前端代码完全隔离各自打包互不影响。拆分的代价是需维护两套登录状态和路由守卫逻辑但换来的是界面交互清晰、后续迭代互不干扰。如果你在改造项目时发现前台风格与后台冲突最合理的操作不是用v-if硬切样式而是直接拆成两个入口视觉上的专业感会提升很多。4.2 赛程数据展示的组件拆分与状态流转赛程展示是前台最容易出彩也最容易出错的地方。竞赛状态会经历未开始报名、报名中、报名结束、评审中、已公布成绩共五个阶段。Vue前端里如果用v-if写死这几个状态后期维护会非常累更好的办法是把“状态判断”收敛到一个统一函数里甚至直接让后端返回状态枚举值前端只做映射。比如赛事状态可以设计为枚举DTO返回给前端const COMPETITION_STATUS_MAP { 0: { text: 未发布, type: info }, 1: { text: 报名中, type: success }, 2: { text: 报名截止, type: warning }, 3: { text: 评审中, type: warning }, 4: { text: 已结束, type: success }, 5: { text: 已取消, type: danger } }组件拆分上我建议把“竞赛列表”“竞赛详情”“报名流程向导”拆成三个独立Vue组件。列表页不需要加载详情页的接口详情页在路由进入时才请求数据这样首屏加载速度会快很多。报名流程向导做成独立组件还有一个好处同一个竞赛在不同阶段可以复用只要传入当前状态组件就会自动显示对应的表单步骤或禁用的“已截止”提示。4.3 文件上传与Excel批量导入的实现要点竞赛作品提交和报名名单导入是前端两个绕不开的文件场景。作品提交涉及到文件类型、大小、文件覆盖三个问题。后端接口接收 MultipartFile 时必须限制扩展名和文件大小不能信任前端传来的文件名因为文件名可以伪造。安全做法是后端生成随机UUID作为存储文件名原文件名单独存到数据库字段里避免路径穿越和重名覆盖。Excel批量导入报名名单则是很多管理员的高频需求。Vue前端用第三方组件读Excel并解析成JSON结构后端接收JSON数组后批量插入。这里很容易踩的性能坑是几千条数据一条条insert耗时可能超过10秒导致前端以为是卡死。改造时我用MyBatis的 batch 模式批量插入单批次300条整体性能提升非常明显。同时Excel中难免有不合格数据比如学号格式不对、姓名缺失我一律采用“先校验全部数据汇总错误行后再导入”的方式而不是碰见一条错误就整批回滚体验差别很大。5. 完整版源码的部署与本地运行从克隆到打开浏览器5.1 环境版本搭配JDK、Node、MySQL要注意的版本对应关系拿到这套完整源码后项目能不能快速跑起来关键在于环境版本。我测试时的搭配是这样的后端JDK用1.8版本SpringBoot是2.x系列MySQL使用5.7或8.0都行Node前端版本建议14以上18以下。如果你直接用JDK 17去跑SpringBoot 2.x大概率会碰到 javax 与 jakarta 包名不兼容的问题Node版本太高部分老依赖的安装也可能报错。个人建议的顺序先启动MySQL导入项目自带的SQL脚本再用 IDEA 导入后端Maven工程等待依赖下载完成后修改数据库连接配置最后启动前端npm install 时如果提示 node-sass 安装失败多半是Node版本和依赖版本不匹配换成 LTS 版本的Node重新安装基本可以解决。我每次调试还会做一件事在 IDEA 中同时打开前端和后端两个窗口后端端口默认配8080前端用Vite或Webpack的代理把 /api 请求转发到后端这样避免开发环境跨域问题。Vue项目里的环境变量文件 .env.development 里配置 VUE_APP_BASE_API 为 /api然后在 devServer 里配置代理跨域问题基本消失。5.2 Nginx SpringBoot的部署配置系统上线时我采用的是标准部署方式前端构建后生成 dist 静态文件由Nginx托管后端打包成 JAR 文件用 systemd 守护进程运行MySQL单独一台服务器或者同机部署。Nginx配置要注意两个细节。第一个是前端路由是history模式非根路径刷新页面会出现404必须在 location / 下加 try_files 重定向location / { root /data/www/competition; try_files $uri $uri/ /index.html; }第二个是API反向代理的路径要注意去掉前缀location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Real-IP $remote_addr; }这里 proxy_pass 末尾的斜杠很关键加了斜杠表示替换 /api/ 前缀不带斜杠则是原样转发配置错了接口全部404。后端JAR的启动内存也要提前规划。我给这套系统的启动参数是 -Xms512m -Xmx1024mTomcat最大线程池根据报名峰值配置为200。MySQL连接池方面HikariCP的 maximum-pool-size 我设为20minimum-idle 为5实测在500人同时报名的场景下没有出现连接等待超时。5.3 拿过源码后最该看的三个包源码到手后我建议不要盲目全读先看三个最关键的位置controller 包、service 包、mapper 包。controller 包能帮你最快了解系统有哪些接口尤其是登录、报名、成绩录入这些核心接口。看的时候重点关注接口的参数校验和返回结构通常这套源码会统一用 Result 对象包裹返回数据了解它的 code、message、data 结构调试时就能快速定位问题。service 包是整个系统的逻辑所在地。竞赛生命周期状态流转、报名事务控制、评分聚合算法全部都藏在这里。改需求时遇到的80%问题最终都要回到service层解决。看service代码时重点找 Transactional 注解出现在哪些方法上这能直接告诉你哪些操作是强一致性的。mapper 包则可以看成数据库设计的逆向映射。XML文件里的SQL比代码更能反映表结构设计的合理程度。我经常提醒接手的开发者先看着mapper把表结构的关系画出来如果发现一个列表查询要join七八张表就要警惕性能问题提前想办法加冗余字段或缓存。6. 实际运行中我踩过的性能与并发问题6.1 报名高峰期的数据库连接池调优高校竞赛报名往往集中在截止前的最后一天学生们的心理都是“再等等看”结果最后几小时流量集中爆发。我上线第一周就遇到了这样的情况报名接口偶尔报出连接超时错误后端日志里全是 HikariPool Timeout 异常。原因很直接HikariCP默认的 maximum-pool-size 是10报名高峰期瞬间并发超过连接池容量新请求只能等待连接释放等待时间超过30秒就直接抛异常。调优方案不是盲目把连接池拉大因为大连接池意味着更多数据库线程和内存占用而是结合实际并发量做估算。我当时的处理是把连接池最大连接数调整到50同时压紧最小空闲连接数到10并在数据库服务器上检查了 max_connections 配置确保全局连接数没有超过MySQL上限。另一个优化点是减少单个请求的数据库连接占用时间。我当时排查后发现报名接口在写入事务前会先执行一个竞赛详情查询这个查询完全可以用本地缓存替代从而让连接池资源集中在真正需要事务的写入操作上。经过这轮调整高峰期再没有出现连接等待超时的情况。6.2 赛程列表慢查询优化竞赛前台首页要展示所有进行中或即将开始的赛事最初实现里每个赛事卡片都会实时统计报名人数逻辑上是在列表循环里调用 count 查询前台一打开后端就执行几十条SQL。页面响应时间一度超过3秒。优化方案分两步。第一步是把“报名人数”这种统计字段冗余到竞赛表里用 registered_count 字段存储报名事务插入成功时同步加一。这种方式牺牲了一点实时精确度但查询性能提升非常明显。第二步是给竞赛表的 status、create_time、start_time 字段建立联合索引因为列表页最常见的筛选条件就是按状态和时间排序。经过优化后首页接口单次查询耗时从300ms降到了30ms左右。这里我可以分享一个经验对中小型系统而言适度冗余永远是性价比最高的性能方案不要一上来就上Redis缓存或引入消息队列。如果一个查询一天只跑几百次完全没必要为了技术炫耀引入复杂组件。6.3 定时任务自动开赛与成绩自动发布竞赛状态不能全靠管理员手动切换否则一旦忘记操作学生看到的赛事状态就会与真实流程脱节。这套源码里引入了一个简单的定时任务模块基于Spring Schedule实现主要做三件事每天凌晨自动检查竞赛状态把报名截止时间已过的竞赛从“报名中”改成“报名截止”把成绩录入截止时间已过的竞赛从“评审中”改为“已结束”定时发送未处理报名审核提醒。定时任务有个容易踩的坑服务器时区不一致导致执行时间偏移。我最初配置的 Cron 表达式是“0 0 2 * * *”本意是每天凌晨两点执行但服务器默认时区是UTC实际执行时间是北京时间早上十点完全打乱了预期。后来统一在Spring配置里指定 timezone Asia/Shanghai问题才解决。还有一点值得注意定时任务里如果有批量修改数据库状态的操作一定要做幂等。比如同样一个“自动关闭报名”任务服务重启后如果重复执行会不断修改已经关闭的赛事虽然结果一样但会产生大量无效的UPDATE记录。我当时在任务执行前加了一个判断条件只有状态为“报名中”且截止时间小于当前时间的记录才会被更新从源头保证了幂等。6.4 证书生成与导出Excel的实现细节竞赛结束后管理员最头疼的就是给一两百号学生发获奖证书。系统里的做法是奖项审核通过后系统按固定模板生成证书图片学生可自行下载同时管理员可以按竞赛导出获奖名单Excel。证书生成我推荐使用后端模板加动态绘制的方式而不是在Vue前端用截图组件。原因是前端截图依赖浏览器环境批量生成时容易遗漏而且分辨率不稳定。后端用固定尺寸的图片模板再把学生姓名、奖项、竞赛名称动态绘制上去输出PNG或PDF文件整套逻辑稳定可靠。导出Excel时不要用后端拼HTML的方式低版本的WPS可能打不开用阿里EasyExcel或POI生成真正的xlsx文件兼容性最好。导出功能也需要关注内存。一次导出全校几千条报名记录如果一次性加载进内存很容易触发老年代GC。我的做法是分页查询每页500条写入一个临时文件最后合并虽然代码稍复杂一些但稳定性有保障。7. 从这套源码延伸到新系统时的改造思路7.1 模块化复用的前提先看清哪些逻辑是通用的如果你手头还有其他管理系统项目比如实验室预约系统、社团活动管理平台这套竞赛源码的很多模块是可以复用的。最通用的是系统管理模块用户管理、角色权限、菜单管理、操作日志。这部分和竞赛业务没有耦合几乎可以直接搬走。竞赛业务本身也有一些通用的领域逻辑比如“报名审核流程”和“提交材料流”可以通过抽象状态机方法来复用。改造时不要急着复制粘贴代码先把原来的 service 层接口梳理成业务能力接口比如 CompetitionService 里的 beginRegistration、closeRegistration、publishAward这些方法名本身就是在描述业务事件移植到其他平台时只需要换掉数据模型逻辑基本可以照搬。7.2 二次开发中最容易被忽略的日志链路没有日志的系统排查问题时就是瞎子。这套源码自带操作日志功能但默认只记录了谁在什么时间操作了什么模块没有记录请求方法的入参和出参。后期排查用户反馈“我明明报名成功为什么名单里没我”这类问题时根本无法还原现场。我给系统增加了一个AOP切面对所有写操作切成操作日志核心信息包括用户ID、操作类型、接口路径、入参摘要、关键返回结果、耗时。这样即使用户反馈问题日志里也能找到完整链路。不过入参记录要注意脱敏尤其是手机号、身份证号这类敏感信息日志里不要明文存储。7.3 数据统计报表的扩展思路竞赛管理系统上线后学校领导最常见的问题是“今年全校参与竞赛人数多少”“哪些院系参与率高”“各赛项获奖情况怎么分布”。这套源码本身带了一个粗糙的统计页面展示总报名人数、总赛事数、总获奖人数三个卡片完全不够用。我扩展时采用的做法是不直接读业务表做实时统计而是每天凌晨由定时任务把统计结果聚合成一个 dashboard_statistics 表。因为这类报表对实时性要求低对查询速度要求高提前聚合报表数据是最合理的。聚合维度包括按院系报名人数排行、按竞赛类别报名趋势、按奖项级别获奖分布。看到只有三个统计卡片时别急着加一堆ECharts图表先想清楚哪些指标是用户真实需要的再动手去接数据。这里的实际体会改造这套系统最大的收获不是把SpringBoot和Vue的代码写得有多漂亮而是理解了竞赛业务里“流程”的重要性。高校竞赛管理表面上是一个信息管理系统本质上是一套带约束的协作流程报名截止之后不能让任何学生再补报成绩公布之前不能让任何人看到结果评分记录要可追溯奖项发布要留痕。系统设计如果脱离这些业务约束技术栈再先进也只是空中楼阁。我每次拿到新的需求清单都会先画一遍状态流转图明确每一步谁可以操作、在什么条件下操作、操作后系统应该发生什么变化。这套竞赛系统的源码让我省掉的不是写增删改查的时间而是这些边界条件踩坑的时间。如果你也用同一套技术栈在做校园类管理平台不妨从竞赛这个场景切入把报名、审核、评分、奖项这条链路走通很多设计思路迁到其他业务里同样能打。