
最近整理了一套企业级大创管理系统的完整源码技术栈就是标题里那套后端 SpringBoot MyBatis前端 Vue数据库 MySQL。如果你在大学信息中心、教务处或者院系里负责创新创业项目的管理或者你正在找一套结构完整、能直接跑起来的毕业设计/课程设计项目这套东西应该能帮你省下不少时间。先说说为什么会有这套系统。以前很多学校管大创项目靠的是 Excel 汇总表加 QQ 群通知。申报书通过邮件来回传文件命名从“最终版”到“最终版2”再到“打死不改版”中期检查的时候办公室主任拿着几个人的 Excel 合并来合并去一个项目状态变了所有人手里的表都不一样。这种状态持续一两年大家基本就被逼疯了。这套系统就是冲着这个痛点来的。它把大学生创新创业训练计划项目的申报、立项、经费、中期检查、结题验收放到一套在线流程里谁审批、批没批、在哪个环节卡住了打开系统一眼就能看到。源码本身我按“企业级”的标准来整理并不是说用户量有多大而是代码结构、权限控制、异常处理、日志审计这些模块都按照正规项目的规范来做拿它当学习蓝本或者直接在它上面做二次开发都会比较顺手。1. 大创管理系统的业务模型它在解决什么管理难题1.1 “大创”项目的完整生命周期大创项目通常分三类创新训练、创业训练、创业实践。每年学校会开放一个申报批次之后项目就进入一条相对固定的流程申报阶段学生填申报书选指导教师提交学院初审。立项评审院系推荐后学校组织专家评审公示立项名单。经费管理立项后下达经费项目执行期间产生报销、调账等记录。中期检查提交阶段性成果检查执行进度发现问题及时终止或整改。结题验收提交结题报告和成果材料论文、专利、软著、获奖等专家验收。成果归档与统计结题后的成果统一归档用于后续评优和按要求上报数据。源码里的功能模块基本就是围绕这段生命周期设计的。你研究这套源码的时候建议先按这条链路走一遍页面再去看代码逻辑会清楚很多。反过来如果你拿到源码第一件事就是找登录接口在哪很容易陷入“会改不会用”的状态。1.2 四种角色对应的核心功能边界系统里的角色我做成了四种覆盖了高校大创管理最常见的分工角色主要操作学生负责人在线填写申报书、上传附件、查看审批进度、提交中期/结题材料指导教师审核学生的申报意向、查看名下项目、对项目的执行过程给出意见院系管理员初审申报材料、管理本院项目、汇总本学院数据、分配指导教师学校管理员配置申报批次、组织立项评审、审批经费、查看全校统计报表这里有个设计细节值得注意学生和教师不是靠“下拉框选人”硬塞进系统的而是通过 user 表与 project_member 关联。一个项目可以挂多个成员其中一个标记为负责人另一个标记为指导教师。这样后面做“某位教师名下有多少个项目”这种统计就很自然一条 SQL 就能拉出来不用在业务代码里做各种循环拼接。1.3 功能模块清单与立项思路核心模块包括系统管理用户、角色、菜单权限、操作日志。项目申报申报批次配置、表单填写、附件上传。审批流程立项、中期、结题三个阶段的审批记录与意见流转。经费管理经费下拨、报销记录、预算剩余额度。成果管理项目关联的论文、专利、软件著作权、获奖信息。消息通知审批结果通过站内信触达相关用户。数据统计按学院、年度、项目类别统计申报数、立项率、结题率。我在动手实现前先把这些模块整理成表格再抽象成一张 project 主表加若干业务扩展表。很多新手容易犯的错是每个模块建一堆独立表模块之间完全没有关联。实际上大创管理最核心的实体只有“项目”其他东西都是围绕项目展开的附属信息建模的时候想清楚这一层后面的代码会简单很多。2. 后端工程拆解SpringBoot MyBatis 的分层协作方式2.1 工程分层与统一返回结构后端工程我保持了最经典的四层结构src/main/java/com/xxx/dachuang ├── controller // 接口层接收参数、调用 service、返回结果 ├── service // 业务层事务边界在这里控制 │ └── impl ├── mapper // 数据访问层对应 MyBatis 的 Mapper 接口 ├── entity // 数据库实体 ├── dto // 接收参数的传输对象 ├── vo // 返回给前端的数据对象 ├── config // 配置类 ├── common // 统一返回、全局异常、分页对象 └── utils // 工具类controller 层只做三件事接收参数、调 service、包装返回值。service 层管业务逻辑和事务。mapper 层只负责 SQL。这样的好处是职责清楚出了问题能快速定位。我见过不少自己写的系统controller 里直接写 SQL看起来一时爽后面改需求的时候恨不得重写整个项目。统一返回结构也是企业级项目的基本操作。我的 Result 对象长这样{ code: 200, message: success, data: ... }分页接口再包一层 PageResult把 total、pageNum、pageSize 一起返回。前端 axios 拦截器里只判断 code 是否为 200不等于 200 就统一弹错误提示等于 401 就跳登录页。这样前后端约定清楚联调的时候能少吵很多架。2.2 MyBatis 配置、TypeHandler 与缓存的使用边界MyBatis 的配置我在 application.yml 里做了集中管理重点有三个mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.xxx.dachuang.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case这个配置要特别留意。数据库字段习惯用 create_timeJava 实体里写 createTime没有这个配置查询结果就映射不上字段全是 null排查半天才发现是驼峰映射没开。调试阶段打开StdOutImpl可以实时看到 SQL 和参数非常有用但是生产环境记得关掉否则日志会刷得很难看。TypeHandler 是 MyBatis 里很多人知道但很少用对的东西。这套源码里用它处理了一个典型场景申报书的“团队成员列表”和“推荐评审专家列表”这些字段是数组结构但为了查询方便直接以 JSON 字符串存到 varchar 字段里。自定义一个 JSONTypeHandler继承 BaseTypeHandler重写 setNonNullParameter 和 getNullableResult一个通用类就能解决所有 List 字段的读写。说实话大部分管理系统的开发用不上复杂的 TypeHandler但这个场景确实比单独建关联表划算。再聊聊缓存。MyBatis 的一级缓存是 SqlSession 级别的在 Spring 项目里每次请求对应一个 SqlSession一级缓存基本不会踩坑。二级缓存是 namespace 级别的很多人一开就出事因为多表联查时缓存内容可能和其他 namespace 的更新对不上出现脏读。我的原则是字典类表项目类别、学院列表可以开二级缓存业务主表一律不开。这套系统用户量撑到几千人没什么压力根本不需要为了“性能优化”去冒缓存不一致的风险。2.3 动态 SQL 撑起项目列表的多条件筛选后台管理项目列表是最常被用到的地方查询条件一般包括状态、批次、学院、负责人、项目编号、起止时间。这种多条件组合查询MyBatis 的动态 SQL 是最合适的工具select idselectProjectPage resultTypecom.xxx.dachuang.entity.Project SELECT * FROM project where if teststatus ! null and status ! AND status #{status} /if if testcollegeId ! null AND college_id #{collegeId} /if if testyear ! null and year ! AND year #{year} /if if testkeyword ! null and keyword ! AND (project_name LIKE CONCAT(%, #{keyword}, %) OR project_no LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY update_time DESC /select这里有几个值得说的细节。where标签会自动处理掉第一个条件前面的 AND省去了写trim的麻烦。if的 test 条件里要同时判断 null 和空字符串否则前端传一个空串过来条件就永远成立查出来的数据是错的。LIKE 查询用 CONCAT 拼接而不是直接在参数里写%是为了避免用户输入特殊字符时产生问题。千万记住一条铁律永远不要自己拼 SQL 字符串传参。别图省事把前端参数直接SELECT * FROM project WHERE status status 塞进 mapperMyBatis 的#{}预编译就是为了防 SQL 注入设计的自己拼字符串等于把这层保护拆了。2.4 事务与并发控制申报批次与经费审批这套系统里有两处必须认真对待事务。一是评审专家批量打分保存二是经费报销审批流程。评审打分这个场景专家一次要提交十几个项目的评分如果中途某个项目分数校验失败前面存的分数已经写进库了后面的一批又没保存数据库里就是半截数据项目状态和评分表对不上。解决方式很直接在 service 方法上加注解Transactional(rollbackFor Exception.class) public void batchSaveScore(ListScoreDTO scores) { for (ScoreDTO dto : scores) { validateScore(dto); scoreMapper.insert(dto); } }rollbackFor Exception.class一定要写。Spring 默认只在遇到 RuntimeException 时才回滚如果业务方法里抛的是检查异常不指定这个参数事务可能不会回滚数据照样写进去。并发控制选的是乐观锁方案。学校的立项名额是有限的多个院系管理员同时提交立项意见时可能出现名额超发。我在 project 表里加了 version 字段更新时带上版本号UPDATE project SET status 3, version version 1 WHERE id #{id} AND version #{version}如果影响行数为 0说明这条数据已经被其他人改过了直接提示“项目状态已被更新请刷新后重试”。大创项目这种规模用乐观锁完全够用没必要引入 Redis 分布式锁那一套重型方案。3. 数据库设计核心表、RBAC 权限模型与索引优化3.1 核心业务表结构与关系数据库一共十几张表核心的一张是 project。这张表承载了项目最主要的属性CREATE TABLE project ( id BIGINT PRIMARY KEY AUTO_INCREMENT, project_no VARCHAR(32) NOT NULL COMMENT 项目编号, project_name VARCHAR(128) NOT NULL COMMENT 项目名称, category TINYINT COMMENT 1-创新训练 2-创业训练 3-创业实践, college_id BIGINT COMMENT 所属学院, leader_id BIGINT COMMENT 负责人用户ID, teacher_id BIGINT COMMENT 指导教师用户ID, budget DECIMAL(12,2) COMMENT 立项经费, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态机, year CHAR(4) COMMENT 申报年度, version INT DEFAULT 0 COMMENT 乐观锁版本号, create_time DATETIME, update_time DATETIME, UNIQUE KEY uk_project_no (project_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status 字段用的是数字枚举不是字符串。数字枚举的好处是排序、比较、统计都很方便而且状态流转全部由后端 Service 控制不允许前端直接传数字改状态。我见过有系统把状态做成 “正在申报、等待审核、审核通过、结题完成” 这种中文字符串前端展示倒是方便了但统计立项率的时候一条WHERE status 3比写字符串稳妥一万倍。审批记录单独建了一张表这样能完整回溯每个项目在每个阶段的审批历史CREATE TABLE approval_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, project_id BIGINT NOT NULL COMMENT 项目ID, stage TINYINT COMMENT 1-申报审核 2-立项评审 3-中期检查 4-结题验收, approver_id BIGINT COMMENT 审批人用户ID, opinion VARCHAR(500) COMMENT 审批意见, passed TINYINT COMMENT 1-通过 0-驳回, create_time DATETIME, KEY idx_project_stage (project_id, stage) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;附件我用 attachment 表统一管理里面存业务类型、业务ID、文件相对路径、原始文件名。文件本体存放在本地上传目录后续要迁移到对象存储时只需要改一个文件存储组件业务表不用动。项目与成员的关系用 project_member 表承接一个项目可以挂多个学生和一个教师。3.2 RBAC 权限模型落表权限模型用的是经典的 RBAC 五张表user、role、user_role、menu、role_menu。menu 表里除了目录和菜单还放按钮权限比如“立项评审”“经费审批”“导出报表”这些操作权限都对应 menu 里的一条记录每条记录有唯一的 perm_code比如project:approve、fund:audit。后端写一个权限拦截器在 controller 方法上标注需要的 perm_code拦截器里检查当前登录用户的角色是否包含对应权限不通过直接返回 403。前端则根据登录时返回的按钮权限列表用 v-if 控制按钮显隐。为什么不直接在 user 表里存一个 role 字段因为真实场景里一个院系管理员可能同时是指导教师也可能被临时指定为某次立项评审的专家。多角色场景靠字符串字段根本撑不住。用 RBAC 以后给用户分配什么角色、角色拥有什么权限全部可以在后台页面里配置不用改代码、不用重启服务。大创管理系统的权限粒度到菜单和按钮这一级完全够用。别一上来就搞数据权限、字段权限那一套那是在给自己加没必要的复杂度。这个系统的用户量总共几百人数据也没到跨部门隔离的程度。3.3 索引规划与慢查询排查经验索引规划我遵循一个原则只给高频查询条件建索引不为“以防万一”建索引。project 表(status, year, college_id)联合索引对应后台项目列表的默认筛选条件。project_no唯一索引项目编号是天然的查询入口。approval_record 表(project_id, stage)联合索引项目详情页要查审批历史。fund_record 表project_id索引经费明细按项目查询。attachment 表(business_type, business_id)索引附件列表按业务查询。排查慢查询的标准操作是先看执行计划。打开 MySQL 命令行执行EXPLAIN SELECT ...重点看 type 字段和 rows 字段。type 是 ALL 说明在扫全表数据量大了必须补索引key 为 NULL 说明索引没生效rows 数字很大说明筛选条件没走索引。还有一个经验关联查询尽量用逻辑外键不要用物理外键。物理外键在插入、更新时会有额外的约束检查数据量一大容易出现锁竞争。很多互联网公司的规范里明确规定禁用物理外键这个系统的用户量虽然没那么大但养成这个习惯没坏处。4. Vue 前端工程路由、权限控制和关键页面实现4.1 前端工程初始化和前后端联调配置前端选的是 Vue 2 Vue CLI 4/5 Element UI这个组合在高校信息化系统里非常常见组件成熟、文档多、遇到问题好搜。工程目录我按功能拆src ├── api // 接口封装按模块拆文件 ├── router // 路由配置 ├── store // Vuex 状态管理 ├── layout // 整体布局 ├── views // 页面视图 │ ├── system // 系统管理 │ ├── project // 项目申报与管理 │ ├── approval // 审批中心 │ ├── fund // 经费管理 │ └── report // 统计数据 └── utils // 工具类和 axios 封装联调配置是很多人会卡住的环节。vue.config.js 里这么配module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }开发环境里前端跑 3000 端口接口请求路径以 /api 开头开发服务器自动转发到后端的 8080。这样配置的好处是前端代码里不写死后端地址只需要请求相对路径。等打包上线时要么用 nginx 做同样的转发要么把前端 dist 直接放到后端 static 目录同端口访问对前端代码零改动。我见过有人在 axios 里硬编码http://localhost:8080联调没问题一上线就得改代码重新打包特别被动。axios 封装里统一做了三件事请求头带 token、响应拦截器判断 code、401 跳转登录。所有接口都走这个封装不会出现某个页面漏处理错误导致白屏的情况。4.2 动态菜单与路由权限的落地方式用户登录成功后调/api/user/permissions接口拿菜单列表和按钮权限存进 Vuex。菜单数据里带上组件路径前端根据这个数据动态注册路由router.beforeEach(async (to, from, next) { const token getToken() if (!token) { if (to.path /login) { next() } else { next(/login) } return } if (!store.state.user.menus.length) { // 刷新页面后路由表为空需要重新加载 const menus await store.dispatch(user/loadMenus) menus.forEach(item { router.addRoute(item) }) next({ ...to, replace: true }) } else { next() } })这里有个 Vue 2 场景下的经典坑动态 addRoute 之后首次跳转会命中 beforeEach如果不加next({ ...to, replace: true })会出现“Redirected when going from X to Y”的警告甚至菜单点击无反应。正确做法就是上面代码里的最后一步重新以当前目标地址触发一次跳转replace 掉上一次的记录。按钮权限的实现更简单后台返回权限码数组写一个全局指令 v-perm在模板里用v-permfund:audit控制元素显隐。核心逻辑就是判断权限码在不在数组里不在就移除这个 DOM 元素。4.3 申报表单、附件上传与审批流的页面写法申报页用的是 el-steps 分步表单第一步基本信息第二步成员与经费预算第三步立项依据第四步上传附件。每一步都有独立的 el-form rules 校验。这里有个体验上的细节草稿状态下的学生随时可以保存修改保存按钮只调 update 接口不改变项目状态点击正式提交时才调 submit 接口把状态从 0 草稿改成 1 待审核。这个区分非常重要否则学生不小心点个提交项目就进审批流了还得管理员手动退回来平添工作量。附件上传用的 el-upload 的 http-request 自定义上传而不是默认的 action 直传。原因是默认上传方式不好带 Authorization 请求头而这个系统所有接口都是 token 鉴权不带 token 一定报 401。自定义 http-request 里手动发一个带 token 的请求上传成功后把返回的文件 ID 回填到表单字段里很稳。审批中心的页面用 el-tabs 按待审核、审核中、已通过、已驳回分类。点进详情时左侧显示项目完整信息右侧按时间倒序列出 approval_record 的审批轨迹审批人、意见、时间一目了然。审核人填意见后点“通过”或“驳回”后端 Service 里做状态校验和流转。再强调一次前端的按钮只是发个请求真正决定状态变更逻辑的必须在后端不然有人抓包改接口系统就是摆设了。5. 源码跑通全流程版本选型、部署步骤与排错清单5.1 推荐环境版本对照这套源码依赖的完整环境版本我整理了一张对照表照着装基本不会出错组件推荐版本说明JDK8 或 11如果源码是 SpringBoot 2.x不要直接用 JDK 17Maven3.6 及以上管理后端依赖SpringBoot2.7.x稳定、生态成熟、问题资料最多MySQL8.0注意时区参数和驱动Node.js16.x对 Vue CLI 4/5 兼容性最好Vue CLI4.5 或 5.0配套 Vue 2 项目npm8.x安装前端依赖有个地方必须单独提醒如果你本地默认装了 JDK 17直接跑 SpringBoot 2.7 的源码大概率会在编译或启动时报错因为 JDK 17 移除了部分 Spring 早期版本依赖的机制。要么换个 JDK 8/11要么把整个项目升级到 SpringBoot 3但升级意味着配置和依赖都要调不建议一上来就折腾。5.2 后端与前端启动的完整步骤后端启动顺序用可视化客户端Navicat、DBeaver、MySQL Workbench 都行连接本地 MySQL新建数据库dachuang字符集选 utf8mb4。导入项目里的sql/init.sql初始化脚本里面包含建表语句和初始管理员账号。修改application.yml里的数据源配置数据库地址、账号、密码。在项目根目录执行mvn clean package -DskipTests。启动产物是 target 目录下的 jar 包执行java -jar target/dachuang.jar。看到 Spring Boot 启动成功的日志后访问http://localhost:8080/api/project/page做一次接口验证。前端启动步骤进入 frontend 目录执行npm install如果报错可以切换国内镜像源再试。执行npm run serve浏览器访问http://localhost:3000。用初始化脚本里设置的管理员账号登录检查菜单和页面是否正常加载。生产环境部署有两个方案。方案 A前端npm run build生成 dist用 nginx 同时托管静态文件和反向代理后端接口location / { root /opt/dachuang/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; }这里try_files那行是必写的。vue-router 开了 history 模式后直接刷新/project/list这种路径如果 nginx 没有 fallback 到 index.html就会返回 404。这个坑几乎每个用 history 模式的人都会踩一次。方案 B 更简单把 dist 拷贝到后端src/main/resources/static目录连同 jar 一起打包。访问 8080 端口后端兜住所有请求接口和页面同源不用配跨域也不用配 nginx。5.3 高频报错排查表我把这段时间被问得最多的报错整理成了一张表报错/现象根本原因解决办法Invalid bound statement (not found)Mapper 接口的 XML 没被打进 jar在 pom.xml 的 build 里配置**/*.xml资源包含Failed to configure a DataSource数据库连接串或账号密码不对检查 url、用户名、密码确认库已创建Public Key Retrieval is not allowedMySQL 8 默认加密插件导致jdbc url 加allowPublicKeyRetrievaltrueCommunications link failureMySQL 没启动或端口不对确认服务已启动端口 3306The server time zone value xxx is unrecognizedMySQL 时区问题jdbc url 加serverTimezoneAsia/ShanghaiAccess denied for user账号密码不对或没有权限核对账号授权库的增删改查权限前端接口 404代理没生效或后端没启动检查 proxy target 端口确认后端进程在运行刷新页面 404history 模式未配置 fallbacknginx 加try_filesUnknown column 报错实体字段与表字段不一致检查驼峰映射配置对照 SQL 里的列名第一行那个 Invalid bound statement 是 MyBatis 项目里最经典的坑。本地 IDE 里跑没问题一打包部署就报“找不到 SQL”十有八九是 XML 文件没有被 maven 打进 jar。在 pom 的 build 节点里加上 resource 配置就能解决。MySQL 8 的 Public Key Retrieval 报错也很烦人。MySQL 8 默认的 caching_sha2_password 认证方式首次连接时要求客户端获取公钥JDBC 驱动默认不允许就会抛这个错。url 后面加allowPublicKeyRetrievaltrue一行配置即可。5.4 后续可以扩展的方向这套源码的扩展点不少挑几个实用方向说文件存储从本地磁盘迁移到 MinIO 对象存储。申报书附件一多本地上传目录会越来越乱备份也不方便。MinIO 加一个 bucket文件即传即存管理界面里还能直接预览。输出报表用 EasyExcel。每年结题后要导出全校项目汇总表手工从页面复制太痛苦后端加一个导出接口前端一个按钮搞定。对接学校统一身份认证。很多高校现在都有 CAS 或 OAuth2 的统一登录把这套系统的登录方式改成对接学校账号体系老师和学生就不用记第二套密码了。审批通知接入邮件。站内信有个天然问题用户不进系统就看不到通知。中期检查提醒这种关键节点加一个邮件发送谁也不会漏掉。把 MyBatis 换成 MyBatis-Plus。如果团队熟悉 MyBatis-PlusCRUD 代码量能少一截分页查询也更顺手。扩展的时候记得守住业务边界。大创系统的核心价值是流程稳定和审批可追溯花里胡哨的新技术加多了反而把系统拖复杂了。最后说点我个人的体会。这套栈我在类似的管理系统上反复用过最大的优点不是性能前沿而是资料全、组件成熟、出问题好排查。你遇到的大部分问题网上都有人踩过并给出了答案。如果准备拿这套源码做毕业设计我建议别急着加功能先把审批状态机和 RBAC 权限这块吃透面试时能把这个讲清楚比堆十个 CRUD 页面都有说服力。部署的时候记得三件事先把初始化 SQL 备份一份数据库连接串里的密码别用弱密码裸奔每次改完代码重新打包前先看看日志有没有告警。把这些做扎实系统跑上一年半载都不会有大问题。祝你能顺利把整套流程跑通。