每年三四月份,一到大创项目申报季,我总能收到不少朋友的吐槽:纸质申报表堆成小山,Excel汇总表改了十几版,评审打分靠人工统计,中期检查材料东一份西一份。这种状态持续久了,其实真正浪费的不是纸,而是学生和老师反复沟通确认的时间。所以当我决定做一个前后端分离的大学生创新创业训练项目管理系统时,第一目标很明确:把立项申报、专家评审、中期检查、结题归档这一整套流程,从"人追着人"变成"流程追着人"。
整个项目采用 SpringBoot + Vue + MyBatis + MySQL 的技术栈,前后端完全分离,源码结构清晰,附带完整部署教程。无论你是想拿它当毕业设计的参考项目,还是想学习一套真实可落地的前后端分离开发流程,这篇文章都会从业务建模、数据库设计、后端接口、前端页面到最终的服务器部署,把每一个关键环节拆开讲透。我会把我实际开发过程中踩过的坑、做过的取舍也一并写出来,希望你能少走几个弯路。
1. 先搞清楚系统在管什么:大创项目管理的业务链条
1.1 从申报到结题,一条完整的项目生命周期
大学生创新创业训练项目,本质上是一套"想法孵化"的流程。学生团队提出创新或创业想法,提交申报书,学校组织专家评审,通过后立项,给予一定经费支持,项目执行过程中需要进行中期检查,最终结题时提交成果材料,由专家组验收。
这套流程在信息系统里落地时,核心就是一条带状态的项目生命周期:
- 项目申报:学生填写项目信息,上传申报书附件
- 立项评审:评审专家查看申报材料,逐项打分,给出是否立项的结论
- 立项与归档:管理员根据评审结果确认立项,生成项目编号
- 中期检查:项目执行到中期时,提交进度报告
- 结题验收:提交结题材料,专家验收
- 成果记录:优秀项目成果统一存档,便于后续统计
听起来不复杂,但实际管理中棘手的是两点:一是同一时间可能有好几百个项目同时在跑,每个项目所处的阶段各不相同;二是每个项目都关联着学生、指导教师、评审专家、管理员四类角色,谁在什么阶段能看什么、能做什么,必须分得清清楚楚。
1.2 为什么不能用通用OA或在线表格凑合
我见过不少学校直接用共享表格收集项目信息,一开始用着还行,项目多了就完全失控。一个项目从申报到结题,申报书、评审表、中期报告、结题材料往往有4到6个附件,Excel里根本没法管理文件版本;评审阶段如果靠表格收集打分,汇总计算很容易出错,而且专家之间还会互相看到对方的打分,这在实际评审中是不允许的。
所以这个系统在设计时,我给自己定了三条业务底线:第一,每个操作节点必须有可追踪的记录;第二,不同角色只能看到自己权限范围内的内容;第三,附件和业务数据要么不关联,要么就严格按阶段绑定。这三条底线直接决定了后面的数据库表结构和接口设计。
2. 技术选型逻辑:为什么是 SpringBoot + Vue + MyBatis + MySQL
2.1 前后端分离的四个直接好处
选前后端分离架构,不是因为"流行",而是这套场景切实需要。首先,开发进度可以并行,后端写接口和前端画页面互不阻塞;其次,部署时可以独立扩展,前端是纯静态资源,后端是无状态服务;第三,同一套后端接口未来可以复用给其他客户端;第四,也是最重要的——评审专家和普通学生的使用场景完全不同,前端可以按角色做完全不同的页面体验,而不需要在后端做复杂的页面渲染逻辑。
实际开发中前后端分离也确实省心。我在这套系统里,后端只负责提供 JSON 数据,前端通过接口消费数据,无论是云端部署还是本地调试,链路都很清晰。
2.2 每一层技术栈的取舍理由
后端选 SpringBoot,本质上是选它的生态和自动化配置。它内嵌了 Tomcat,打一个 jar 包就能跑,不需要单独装 Servlet 容器;Starter 机制把数据库连接、事务管理、Web 配置都自动化了,比起传统的 SSM 项目省掉了大量 XML 配置。这也意味着即使是新手,也能在半小时内把后端项目跑起来。
MyBatis 和 JPA 之争,在这个项目里我选了 MyBatis。原因很实际:大创项目的列表查询条件特别多——按学院、按项目类型、按状态、按立项年份,这种带动态条件的 SQL,MyBatis 写起来最直观可控。而且它的 XML 映射文件方便做多表关联查询,项目成员、评审记录、附件信息这种一对多关系,一条 SQL 就能查干净。相比之下 JPA 在这种动态查询和复杂映射场景下反而要写很多 Specification,维护成本更高。
数据库用 MySQL,是因为它对热数据、事务、并发更新支持得很成熟,加上这套管理系统本来就不是超大并发场景,5.7 或 8.0 版本完全够用。字符集我直接定了 utf8mb4,避免学生申报书里写生僻字或特殊符号时出现乱码。
2.3 项目目录结构规划
我的前后端目录是这样组织的:
后端项目(springboot-project):
- controller:接口层,只做参数接收和结果返回
- service:业务逻辑层,事务都在这层控制
- mapper:MyBatis 的数据访问接口
- entity:数据库实体
- vo:给前端用的视图对象
- common:统一返回结果、异常处理、工具类
- config:拦截器、CORS、上传配置
前端项目(vue-project):
- src/api:每个模块的接口调用
- src/router:路由与菜单配置
- src/views:页面组件
- src/components:公共组件(文件上传、富文本等)
- src/store:登录状态和用户信息的全局状态管理
这样的分层,核心思想就是把"接口-业务-数据"全部解耦,哪个环节出问题,可以快速定位到具体模块,不会出现改一个需求就全局牵一发动全身的情况。
3. 数据库设计:五张核心表撑起整个业务流转
3.1 用户、角色与权限表
用户体系我没有用太复杂的设计。考虑到这个系统的角色相对固定,我没有单独建动态权限表,而是用 user 表和 role 字段来解决:管理员、指导教师、学生、评审专家,各一个角色编码。
用户表的核心字段包括:用户ID、学号/工号、姓名、密码(MD5加盐或BCrypt加密)、角色编码、所属学院、联系电话、邮箱、状态。角色字段直接存编码,比如 0 表示管理员,1 表示教师,2 表示学生,3 表示专家。这样在写接口时,通过角色编码就能快速做权限判断。
3.2 项目主表:状态机是灵魂
项目主表是整个系统最核心的一张表,我把它命名为 project。核心字段如下:
| 字段 | 说明 | 备注 |
|---|---|---|
| id | 项目ID | 主键 |
| project_no | 项目编号 | 立项后生成,如 2024XC001 |
| project_name | 项目名称 | |
| project_type | 项目类型 | 创新训练/创业训练/创业实践 |
| level | 项目等级 | 校级/省级/国家级 |
| college | 所属学院 | |
| teacher_id | 指导教师ID | 关联user表 |
| leader_id | 项目负责人ID | 关联user表 |
| budget | 经费预算 | 单位元 |
| status | 项目状态 | 0草稿 1待评审 2已立项 3中期检查 4待结题 5已结题 6已终止 |
| apply_time | 申报时间 | |
| create_time | 创建时间 |
status 字段其实就是一条状态机。所有业务操作在代码里最终都变成了"状态流转 + 日志记录"两件事。比如学生提交申报,状态从 0 变成 1;管理员确认立项,状态从 1 变成 2。为了做得到操作可追溯,我加了一张 project_log 表,记录每一次状态变更的操作人、操作时间、操作前后状态、操作备注。这张表在后续查"某个项目为什么被终止"时,能省下大量扯皮的时间。
3.3 评审表与附件表:一对多的经典设计
一个项目会被多个专家评审,所以评审表和项目表是典型的一对多关系。评审表核心字段包括:评审ID、项目ID、专家ID、创新性得分、可行性得分、完成度得分、总分、评审意见、评审状态、评审时间。这里我特意把各项得分做成独立字段,而不是合成一个 JSON,原因是管理者经常要统计"各项目的平均创新性得分",独立字段可以直接用 SQL 做聚合,效率高得多。
附件表则是我全系统统一设计的一张通用表,无论是一开始的申报书、中期报告,还是结题材料,都往这一张表里存。核心字段包括:附件ID、关联业务ID、附件类型(1申报书 2中期报告 3结题报告 4成果附件)、文件名、存储路径、上传人、上传时间。统一附件表的好处是上传、下载、删除逻辑只需要写一套,后期做存储迁移也方便。
3.4 索引与字段设计上的几个细节
数据库字段设计上我有几个习惯:时间字段统一用 datetime 类型,避免时区问题;金额字段用 decimal(10,2);文本类字段如果不确定长度就选 text 类型而不是硬性限定 varchar。
索引方面,project 表我在 status 和 college 上建了普通索引,因为首页的项目列表几乎都是按状态或学院过滤的;评审表的 project_id 建索引,避免联表统计时全表扫描。project_log 表则按 project_id 建索引,否则随着日志增多,查询历史轨迹会越来越慢。
4. 后端核心实现:认证、统一返回、分页查询与文件上传
4.1 基于 JWT + 拦截器的登录认证
这个项目我最终没有引入 Spring Security,因为系统的权限模型并不复杂,用 Spring Security 反而要写不少配置。我的方案是 JWT + HandlerInterceptor,简单直接,也方便在演示时把整个链路讲清楚。
登录流程是这样的:用户提交用户名和密码,后端校验通过后生成一个 JWT Token,Token 里放入用户ID、用户名、角色编码,设一个合理的过期时间,比如3小时,然后返回给前端。前端把 Token 存在 localStorage 里,每次请求都在请求头带上 Authorization: Bearer 。后端写一个拦截器统一校验 Token,解析出用户信息后放到 ThreadLocal 里供业务方法随时取用。
拦截器里需要注意放过登录接口和静态资源,只拦截 /api/** 下的业务接口。我这里贴一下关键思路:
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BusinessException(401, "未登录或登录已过期"); } UserInfo user = JwtUtil.parseToken(token.replace("Bearer ", "")); UserContext.set(user); return true; } }提示:生产环境建议把 Token 有效期缩短并配合刷新机制,这个项目里为了演示方便我设了相对长的时间,实际落地时可以再加一层 Redis 做 Token 黑名单管理。
4.2 统一返回结果与全局异常处理
如果每个接口都返回不同的数据结构,前后端联调会非常痛苦。所以我设计了一个 Result 类,所有接口统一返回 { code, message, data } 三件套。code 为 200 表示成功,其它为各类错误码,前端拿到后可以统一处理。
配合统一返回的是一套 @RestControllerAdvice 全局异常处理。我在 service 层抛出的业务异常,都会被捕获并转换成规范的响应。这样做的好处是,前端不需要在每一个接口里都写一遍报错处理逻辑,统一弹提示即可。最关键的是,数据库层面异常会被包装成"系统繁忙"这类信息,避免直接把 SQL 报错信息暴露给用户。
4.3 多条件分页查询的 MyBatis 写法
项目列