如果你点进这篇文章,大概率是在准备做一个大学生科技竞赛管理系统,或者在找Java+SpringBoot+SSM方向的项目参考。这个标题我太熟了,很多同学拿到题目后的第一反应是“这不就是个后台管理CRUD吗”,但实际动手就会发现,竞赛管理系统比普通管理系统要复杂不少,难点集中在几个地方:赛事状态怎么流转、多人组队报名时如何保证数据一致、多评委打分怎么统计排名、不同角色的权限边界如何控制。这篇文章我就围绕这套系统,把从需求拆解到数据库设计,再到核心模块代码实现和项目部署交付的完整过程整理出来,全部是一个可复现项目的真实经验。
我默认你已经有Java基础和基本的SpringBoot使用经验,但即使你刚学完Spring,这篇文章里涉及的知识点也足够你照着一路写下来。我讲的每一个坑,都是实际开发中真真切切遇到的,不是从教程里抄来的。
1. 项目到底在做什么:需求拆解与整体设计
1.1 业务场景与功能全景
大学生科技竞赛管理系统,核心不是“展示赛事”,而是把赛事从发布到结束的全过程搬到线上。实际校园场景里,一个竞赛的完整链路大概是这样的:教务处或学院发通知,学生组织队伍报名,队伍提交作品,老师担任评委打分,最后管理员公示成绩和获奖名单。传统做法是微信通知群加Excel表格加邮件收作品,信息零散、容易出错、统计也慢。系统的价值就是把这些动作全部集中到一个平台里,每个角色只看到和自己相关的内容。
按角色来拆功能,大致是:
- 管理员:用户管理、赛事创建与发布、赛事审核、公告管理、数据统计
- 学生:浏览赛事、在线报名、创建或加入队伍、上传作品、查看成绩
- 评委/教师:查看分配给我的评审任务、对作品在线打分、填写评语
只看这一层还不够。我建议你按“赛事生命周期”来重新组织需求,这对后面的表结构和代码设计帮助非常大。一个赛事从无到有会经历以下阶段:
- 赛事创建:管理员录入赛事名称、主题、报名时间、作品截止时间、参赛限制
- 赛事发布:审核通过后状态变为报名中,学生端可见
- 报名组队:学生选择赛事,创建队伍或加入已有队伍,队长提交报名
- 作品提交:报名审核通过后,队伍在截止时间前上传作品
- 评审打分:管理员分配评委,评委在线打分并给出评语
- 结果公示:系统根据评分规则计算成绩,管理员设置公示状态
生命周期是数据库状态机的设计依据,也是答辩时最能讲出东西的部分。很多同学的项目做完只能说“登录、增删改查”,但你如果能把这个生命周期讲清楚,整个项目的档次一下就上来了。
1.2 为什么选择SpringBoot+SSM这套技术组合
项目标题里的技术栈是Java+SpringBoot+SSM。有人会问,SSM不是指Spring+SpringMVC+MyBatis吗?SpringBoot已经是整合框架了,为什么还要强调SSM?这里要解释清楚,也是答辩时容易被问到的问题。
这套组合的实际含义是:SpringBoot负责自动装配和快速启动,SpringMVC负责Web层接口处理,MyBatis负责持久层SQL操作。也就是说,项目整体跑在SpringBoot容器里,但内部的分层架构仍然沿用SpringMVC+MyBatis的经典模式。这种组合在高校课程和中小型项目里非常常见,你后面接手的人也好理解。
选型理由我再展开说说:
- 学习成本低。SSM本身是计算机专业课程体系里的主流内容,源码给学弟学妹维护,或者你自己过几个月再回来看,都不会有陌生感。
- SQL可控性强。竞赛系统里大量涉及多表查询和聚合统计,比如按赛事统计报名人数、按评委分组算平均分、去掉最高分最低分这种排名计算。用MyBatis写SQL非常直观,如果用JPA的Criteria查询,复杂报表会让你头疼到怀疑人生。
- 生态成熟。无论你用的是IDEA、Maven、MySQL,还是最后部署到服务器,这套技术栈的资料和技术帖子都多到看不完,遇到问题容易排查。
我印象很深的是有一次带队伍做比赛管理系统,队友一开始强烈推荐JPA,理由是实体类写得爽、自动建表方便。结果做到评审排名那一步,要按赛事分组统计、去掉最高最低、算平均分还要处理并列条件,JPA的查找方案写出来又长又难调。最后我们还是把持久层换回了MyBatis。这不是说JPA不好,而是团队要选择自己最可控的技术。
提示:毕设项目选型,求稳优先。你如果非要尝试WebFlux、JPA或其它非主流组合,不是不行,而是答辩时必须额外解释技术差异,这部分工作量其实是没必要的。
1.3 分层架构与项目包结构
项目不要所有代码堆在一个包下,分层清晰是代码质量的第一道底线。按SSM的惯例,工程建议拆成下面这些包:
src/main/java └── com.example.contest ├── config // 配置类:拦截器、跨域、文件上传映射等 ├── controller // 接口层,只做参数接收和统一返回 ├── service // 业务层,事务和核心逻辑都在这层 ├── dao // MyBatis的Mapper接口 ├── entity // 数据库实体类 ├── dto // 前端交互所需的数据对象 ├── vo // 视图对象/统计结果对象 └── common // 常量、统一返回、异常类、工具类分包的核心原则就是:controller层只做参数接收和校验,service层负责业务逻辑和事务管理,dao层只做数据库访问。很多同学图省事把SQL和业务判断全写在controller里,开发前期确实很爽,但后面一旦要加权限、加日志、加多角色判断,就会变成一团乱麻,你自己都不敢去改。
2. 数据库设计与核心表结构:决定项目上限
2.1 核心数据表与字段设计
数据库设计是整个系统的地基。我见过太多项目做到一半推倒重来,几乎都是因为表结构没想清楚。竞赛管理系统核心表不算多,但每张表都有它需要注意的设计细节。
核心表可以分成下面几张:
- 用户表 user:主键、用户名、密码、姓名、学号/工号、角色类型(学生/教师/管理员)、院系、电话、创建时间
- 赛事表 competition:主键、赛事名称、简介、赛事类型(挑战杯/互联网+/专业赛)、报名开始时间、报名截止时间、作品截止时间、状态、队伍人数上限、创建人
- 团队表 team:主键、赛事ID、队名、队长ID、创建时间
- 团队成员表 team_member:主键、团队ID、成员ID、加入时间、成员角色
- 报名表 registration:主键、团队ID、赛事ID、提交时间、审核状态、审核意见
- 作品表 work:主键、团队ID、赛事ID、作品名称、简介、文件路径、状态
- 评审表 review:主键、作品ID、评委ID、分数、评语、评审时间
- 公告表 notice:主键、标题、内容、发布人、发布时间
这里有四个设计错误的坑要专门提醒:
- 角色字段不要用字符串。很多表里会出现“管理员”“admin”“ADMIN”三种写法,联调时一查一个准。建议用数字0/1/2,然后在Java常量类里统一管理。
- 状态字段必须加注释。比如赛事表status,0代表草稿、1代表报名中、2代表评审中、3代表已结束。加了注释,前后端联调时沟通成本会低很多。
- 不要乱加物理外键。表之间用逻辑关联(字段存ID)就够了,别真去加FOREIGN KEY约束。竞赛系统里删除和更新场景很多,物理外键经常导致删不动、更新报错。
- 每张表都要有create_time和update_time,条件允许就加逻辑删除字段deleted。逻辑删除在数据统计时很有用,哪怕业务上不要求,写代码时留着也能防止误删数据没法恢复。
2.2 示例DDL:一张关键表的建表思路
这里给出竞赛系统最重要的一张表——赛事表的MySQL建表DDL,后续表结构可以参考这个思路去写:
CREATE TABLE `competition` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `name` varchar(100) NOT NULL COMMENT '赛事名称', `description` text COMMENT '赛事介绍', `type` varchar(50) DEFAULT NULL COMMENT '赛事类型:挑战杯/互联网+/专业赛等', `signup_start_time` datetime DEFAULT NULL COMMENT '报名开始时间', `signup_end_time` datetime DEFAULT NULL COMMENT '报名截止时间', `work_deadline` datetime DEFAULT NULL COMMENT '作品提交截止时间', `max_member` int(11) DEFAULT '5' COMMENT '队伍人数上限', `status` tinyint(4) DEFAULT '0' COMMENT '状态:0草稿 1报名中 2评审中 3已结束', `create_by` bigint(20) DEFAULT NULL COMMENT '创建人ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `deleted` tinyint(4) DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='赛事表';字段选择上有一个经验可以分享:类型type为什么用varchar而不用int?因为赛事类型后续很可能要扩展名称,用int你还得维护一张字典表,对毕设来说是过度设计。而状态status为什么用tinyint而不是varchar?因为状态是定死的枚举,用数字在代码里比较高效,也不怕大小写问题。这种选择没有绝对的对错,关键是全项目的字段规范要统一。
2.3 赛事状态机与时间校验逻辑
有了表结构,接下来是业务层的核心:状态机。竞赛系统的所有操作都应该围绕状态流转,而不是“想在哪操作就在哪操作”。
定义清楚状态机:
- 0草稿:管理员创建后未发布,学生不可见;
- 1报名中:赛事已发布,学生可报名组队,但还不可提交作品;
- 2评审中:报名截止、作品提交截止,评委开始打分;
- 3已结束:评审完成,结果公示,系统进入只读阶段。
写业务接口时必须先判断状态:报名接口只能在status=1时执行;提交作品接口要保证当前时间早于work_deadline;评审接口只能在status=2时调用。为什么状态和截止时间都要判断?因为状态是管理员手动改的,可能存在时间差;而截止时间是硬性边界,规则上用时间判断永不失效。
我见过一个经典bug:某同学在赛事状态从“报名中”切到“评审中”后,还能通过直接改前端URL调后端提交作品接口。原因是代码只做了前端按钮隐藏,完全没有在service层校验赛事状态。这是一个典型的“后端信任前端”的错误。所有重要操作都必须在后端接口里做二次校验,身份校验、状态校验、时间校验一个都不能少。
在并发场景下,还要考虑两个学生同时报名同一个赛事,都通过了“未报名”校验,结果各建了一支队伍、各提交了一条报名记录。解决办法可以是给team表的competition_id和leader_id加唯一索引,或者对赛事状态字段做乐观锁控制。毕设做到唯一索引这一层基本就够用了,但如果答辩时能说出索引兜底和乐观锁、悲观锁的区别,这一个点就值得写进论文里。
3. 核心功能模块的代码实现细节
3.1 登录认证与角色权限控制
系统里有三类角色,权限控制如果做不好,学生就能改别人的作品、评委就能看别人的打分、普通用户能进管理后台。这个项目的权限方案我不建议直接上Spring Security,虽然它是标准方案,但配置复杂度高,对毕设来说可解释性差。我推荐用“登录Token + 拦截器 + 自定义注解”的轻量方案,关键在于你能把原理讲清楚。
流程是这样的:用户登录成功后,用一个UUID生成token,以token为key,userId为value存入Redis并设置过期时间;前端把token放请求头;后端写一个全局拦截器,统一校验token是否存在、是否过期,再根据接口上的角色注解判断当前用户是否有权限。
拦截器核心代码类似这样:
@Component public class AuthInterceptor implements HandlerInterceptor { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (!StringUtils.hasText(token)) { throw new BusinessException(401, "未登录"); } Object userId = redisTemplate.opsForValue().get("login_token_" + token); if (userId == null) { throw new BusinessException(401, "登录已过期"); } // 校验角色 if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class); if (requireRole != null) { User user = userService.getById((Long) userId); if (!Arrays.asList(requireRole.value()).contains(user.getRole())) { throw new BusinessException(403, "无权限访问"); } } request.setAttribute("currentUserId", userId); } return true; } }注意一个小细节:如果Redis里存的token过期时间是30分钟,用户连续操作到第29分钟时token刚过期,下一秒请求就会401,体验很糟糕。正确做法是在拦截器里统一做“续期”,每次请求都会刷新剩余时间,用户只要在用,就不会莫名掉线。
密码存储必须用BCrypt加密,不要用MD5。MD5虽然能跑,但答辩时被问“密码安全怎么做”就非常尴尬。SpringBoot自带spring-security-crypto,单独引入后可以直接使用BCryptPasswordEncoder,加盐逻辑都是封装好的,调用简单。
3.2 赛事报名与组队流程的事务处理
报名是竞赛系统里写操作最密集的模块。一个学生报名参加团队赛,后端要同时做三件事:创建队伍、把当前用户插进成员表、写入报名记录。任何一个步骤失败,其他步骤都要回滚,否则会出现“有队伍没队长”或者“报名记录残缺”的脏数据。
解决办法很简单:在service方法上标注@Transactional。但这里有个很多人不知道的坑,@Transactional默认只在RuntimeException时回滚,如果你自己在业务里throw new Exception("xx"),事务不会回滚。所以项目里一定要自定义一个BusinessException继承RuntimeException,所有业务异常都抛它,这样才能保证事务及时回滚。
核心报名逻辑示例:
@Transactional(rollbackFor = Exception.class) public void signUp(Long competitionId, Long userId, String teamName) { // 1. 校验赛事状态 Competition competition = competitionMapper.selectById(competitionId); if (competition == null || competition.getStatus() != 1) { throw new BusinessException("当前赛事不在报名时间内"); } // 2. 防止重复报名,数据库唯一索引兜底 if (registrationMapper.existsByUserAndCompetition(userId, competitionId)) { throw new BusinessException("请勿重复报名"); } // 3. 创建团队 Team team = new Team(); team.setCompetitionId(competitionId); team.setName(teamName); team.setLeaderId(userId); teamMapper.insert(team); // 4. 队长也是团队成员 TeamMember member = new TeamMember(); member.setTeamId(team.getId()); member.setUserId(userId); teamMemberMapper.insert(member); // 5. 写入报名记录 Registration registration = new Registration(); registration.setTeamId(team.getId()); registration.setCompetitionId(competitionId); registration.setStatus(0); registrationMapper.insert(registration); }并发情况下,两个请求同时走到第2步,数据库层面并没有阻止,它们都会创建队伍并生成报名记录。杜绝这个问题最简单的方式是给team表加唯一索引,比如competition_id和leader_id的联合唯一索引;如果你的系统还要求一个赛事有队伍数量上限,那就需要更细的并发控制,比如对赛事行加乐观锁版本号,更新时校验版本号。
3.3 文件上传与作品提交
作品提交这块最麻烦的是文件上传。学生上传的可能是一个压缩包、PDF或视频,文件大小从几百KB到上百MB都有可能。如果你的系统前后端分离部署,还会有跨域限制问题。
建议做一个独立的上传接口,统一接收multipart文件,校验后保存到本地磁盘或对象存储。如果是毕设,存本地磁盘完全够用。文件名不要用用户原始文件名,容易中文乱码重名,建议用UUID重命名,再把原始文件名记进数据库。
SpringBoot默认单文件上传上限是1MB,必须重新配置:
spring: servlet: multipart: max-file-size: 200MB max-request-size: 210MB文件类型不能只看后缀名,最好用MIME类型做白名单,至少后端要拦掉exe、sh这类可执行文件。保存目录也不要放到项目源码目录里,否则打包后运行会找不到路径。我习惯在配置类里做静态资源映射:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadDir + "/"); } }文件上传还有一个隐蔽的问题:如果文件先落盘、后写数据库,数据库写失败,磁盘上就留下一个孤儿文件。规范做法是在service里try-catch,数据库写失败时立刻删除已保存文件,或者用定时任务定期清理未关联的文件。对于毕设,前者实现简单,也不容易出问题。
3.4 多评委打分与成绩统计
评审打分是系统里另一个业务难点。一个作品通常由多个评委共同评分,最终成绩不是简单平均,而是有规则的。常见规则是去掉一个最高分和一个最低分,再取平均分。不同赛事还可能有评委数量要求、权重分配差异。
评审表设计上,每个评委对同一个作品只允许有一条记录,用(work_id, judge_id)做唯一索引,防止同一评委重复打分。统计最终成绩时,我用下面这条SQL:
SELECT work_id, CASE WHEN COUNT(*) <= 2 THEN AVG(score) ELSE (SUM(score) - MAX(score) - MIN(score)) / (COUNT(*) - 2) END AS final_score FROM review WHERE deleted = 0 GROUP BY work_id;注意,如果作品只有一位评委打分,去掉最高最低后分母会变成0,SQL直接报错。所以统计前必须查一次评审数,少于最低评委数的作品要标记为“待评审”,不参与排名。
排名展示还有一个让人摸不清的坑:当两个作品最终得分相同,页面顺序会来回跳动。究其原因是没有定义并列时的排序规则。系统里必须补上二级排序条件,比如作品提交时间更早的优先、作品编号更小的优先,否则用户刷新两次结果看到排名变了,会质疑系统的正确性。
4. 开发与联调中最容易踩的坑
4.1 SpringBoot版本与依赖版本匹配
SpringBoot版本不要太激进。现在最新的3.x版本要求JDK17及以上,而很多高校毕设环境还是JDK8,团队其他人用的工具链也未必跟上。我强烈建议就用SpringBoot 2.7.x + JDK8 + MyBatis + MySQL 5.7/8.0这套组合,网上资料多,问题少,稳定。
版本选择不当最典型的表现是:
- 引入mybatis-spring-boot-starter后,启动直接报ClassNotFoundException;
- Druid连接池在JDK17下反射异常;
- javax.servlet.* 变成 jakarta.servlet.*,网上教程里的代码复制过来编译不过。
一句话:能用稳定版就不用最新版。技术选型的首要任务不是追新,而是降低风险。
4.2 MyBatis XML与Mapper扫描问题
MyBatis实现SQL有两种方式,注解和XML文件。我的建议是全部统一用XML,复杂动态SQL在注解里写会让代码膨胀到没法看。
最频繁出现的报错是“Invalid bound statement (not found)”。碰见这个先按顺序排查以下四个点:
- application.yml里mybatis.mapper-locations是否配置为classpath:mapper/*.xml;
- XML文件是否真的放在了src/main/resources/mapper目录下;
- XML文件的namespace是不是和Mapper接口全限定名一致;
- XML里每条语句的id是否和接口方法名一致。
这个报错在SSM开发里的出现率真的可以排第一,你只要遇到,就可以直接按这个顺序过一遍,基本都能解决。
4.3 事务不生效与自调用问题
@Transactional不生效,最常见原因是同一个Service类里内部方法互相调用。Spring事务基于AOP代理实现,内部调用走的是this.method(),不走代理,注解自然失效。
解决方式有两种:
- 把需要事务管理的方法放到另一个Service类中,由外部类调用;
- 简单粗暴但有效:自己类里注入自己的代理,不推荐但能解决问题。
还有一个隐蔽的场景:在事务方法里catch住异常后返回false,事务感知不到任何异常,没有回滚。记住,事务方法里能不在catch中吞异常就尽量不吞,如果要catch,必须手动抛出RuntimeException。
4.4 跨域配置与时间序列化
前端用Vue开发、后端跑8080端口的话,势必遇到跨域问题。SpringBoot里的核心配置是先允许跨域请求,再设置允许携带凭证:
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }设置allowCredentials(true)之后,allowedOrigin不能直接写“*”,只能用allowedOriginPattern,否则前端会直接报错。这个细节踩坑率极高。
另一个容易被忽略的是时间字段的序列化问题。后端的LocalDateTime返回JSON后可能是“2024-07-15T10:30:00”这种ISO格式,前端显示不好看。统一在配置里处理:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8如果你实体用的是LocalDateTime而不是java.util.Date,上面的date-format不生效,需要额外加Jackson序列化配置,或者干脆统一用Date类型。联调阶段做好这步,能省很多沟通时间。
4.5 常见问题速查表
我把开发中常遇到的问题整理成了一个速查表,建议收藏:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 登录后请求接口报401 | token过期或Redis中无记录 | 检查拦截器续期逻辑、Redis连接 |
| 中文文件名上传后乱码 | 未对原始文件名做处理 | 用UUID重命名保存,原始名存数据库 |
| 接口响应慢、日志打印SQL | 查询未走索引 | explain查看执行计划,状态字段加索引 |
| 工程启动后静态资源404 | 静态资源映射配置错误 | 检查addResourceHandlers配置 |
| 前后端联调一直CORS报错 | 允许凭证后仍使用通配符域名 | 改用allowedOriginPattern |
| 打包后页面显示Whitelabel Error | 前端打包产物未放对目录 | 将dist目录内容复制到resources/static |
5. 打包部署与项目交付的完整经验
5.1 Maven打包与生产环境部署
开发完毕后,用Maven打包成jar包是SpringBoot单应用最标准的部署方式。IDEA里先执行clean,再执行package,就能在target目录下拿到一个可执行的jar包。
生产环境部署时,我用的是下面这条命令:
nohup java -jar contest-system.jar --spring.profiles.active=prod > app.log 2>&1 &两个建议:
- 配置必须分环境。开发环境application-dev.yml与生产环境application-prod.yml分开,数据库地址、密码、Redis连接不要混在一起,防止误连测试库。
- 如果前端用了Nginx反向代理,大文件上传除了Spring的multipart限制,还要修改Nginx的client_max_body_size,这一项经常漏。
数据库初始化用SQL脚本一次性执行,不要依赖MyBatis自动建表。自动建表虽然省事,但字段注释、索引、唯一约束完全不受控,后期调整表结构非常痛苦。SQL脚本跟着代码走,这才是可复现的项目交付方式。
5.2 源码+LW+调试文档+讲解的交付物搭配
如果这个项目是毕业设计或类似的课程设计,交付物绝不只是“能跑的代码”。一个完整的交付物应该由四部分组成:
- 源码:代码要有必要的注释,能独立跑起来,关键模块不要写得太散;
- LW(论文/设计报告):至少包含系统设计、表结构设计、核心流程图、界面截图、测试结果五章。论文不是越厚越好,而是要能讲清楚“为什么这么设计”;
- 调试文档:环境要求、启动步骤、端口配置、默认账号权限、常见错误处理。这份文档其实是写给你自己的,答辩现场环境出问题时能快速恢复;
- 讲解准备:能对着页面把核心表结构、状态流程、一个核心接口的代码逻辑讲明白。提前准备一套演示数据,包含几个不同状态的赛事、若干支队伍、已有的评审分数,避免现场临时造数据。
我在交付前会坚持做一次“全流程实测”:用一个新账号把赛事发布、报名、组队、作品提交、评审、结果公示完整走一遍,每一步都截图留存测试数据。这件事做一遍,等到写论文或者答辩时,你就能感到奇迹般的轻松。
5.3 项目目录结构与文档组织建议
为了让接手的人(评审老师、学弟学妹)快速理解项目,源码里建议保留一个README,并把目录结构规范化:
contest-system ├── src/main/java │ └── com/example/contest │ ├── config │ ├── controller │ ├── service │ ├── dao │ ├── entity │ ├── dto │ └── common ├── src/main/resources │ ├── mapper // MyBatis XML │ ├── static // 前端打包文件 │ └── application.yml ├── sql │ └── contest.sql // 数据库初始化脚本 └── README.md为什么单独把sql独立出来放在一个目录?因为很多同学建表习惯直接在Navicat里手工创建,最后交代码时连初始化脚本都没有,换一台电脑就很难快速还原整个环境。这个习惯一定要改,所有DDL都写进SQL文件,跟着代码一起走,项目才谈得上“可交付”。
带过不少同学做这类竞赛管理系统,我发现大部分人的问题不在技术难度,而在需求理解不透就开始写代码,最后东一改西一改,越改越乱。你如果能先把角色、状态、流程这三件事想清楚,工程结构按照分层的思路搭好,再动手去写具体功能,整套开发节奏就会顺畅很多。系统本身不复杂,但把细节处理好,它就是一个能拿得出手的完整项目。