基于SpringBoot+Vue的科创项目管理系统设计与部署全解析
2026/9/9 3:44:10 网站建设 项目流程

做这个大学生科创项目管理系统,不是一时兴起。带比赛、报项目、中期检查、结题验收,这一整套流程里,文档靠微信传来传去、申报靠邮件加压缩包、审批进度全靠私聊催问——但凡带过一届学生,都知道这有多崩溃。所以我才决定做一套前后端分离的在线管理系统,后端用SpringBoot+MyBatis,前端用Vue,数据放MySQL,把项目申报、审核、中期检查、结题归档全部搬到线上。

写这篇博客,是想把整套系统的设计思路、核心代码、数据库结构以及从本地联调到服务器部署的完整过程讲清楚。如果你正好需要做类似的管理系统,比如毕业设计、课程设计,或者想自己搭一套前后端分离的项目练手,那这篇内容基本可以让你少走一大截弯路。我默认你有一点Java和前端基础,但一些关键细节我会讲到比较细,跟着做就能跑通。

1. 这个系统要解决什么:三个真实痛点决定功能边界

很多同类系统一上来就堆功能,结果做出来体积大、逻辑乱、没人用。我在动手之前,先梳理了大学生科创项目管理里最痛的三件事,所有功能都是围绕它们设计的。

1.1 项目流转靠人肉跟踪,进度完全不可控

以前一个项目从申报到结题,要经过指导教师、学院管理员、学校管理员至少三个角色的审批。每个环节都靠线下催办,材料在QQ、微信、邮箱之间来回传。经常出现的情况是:学院管理员不知道自己学院有哪些项目正在申报,学校管理员不知道某个项目卡在哪个环节,学生只能挨个问。

所以系统里必须有一张清晰的状态流转图:草稿 → 待审核 → 审核通过/驳回 → 中期检查 → 结题 提交 → 已结题/终止。每个状态变更都要留痕,谁在什么时间做了什么操作,全部记录在日志表里。这一条是硬需求,直接决定了表结构里要有audit_logproject_status_log两张表。

1.2 材料格式五花八门,归档时无从下手

学生的申报书有的用Word 97版、有的用WPS另存、有的干脆发个腾讯文档链接,格式千奇百怪。到了结题归档的时候,材料根本没法统一整理。

解决办法是系统统一模板上传——把申报书、中期报告、结题报告的模板做成固定文件,学生下载后按模板填写,再上传PDF或Word。服务器端加文件格式和大小校验,只允许特定扩展名,单个文件限制在20MB以内。文件名也做了统一规则:项目编号 + 材料类型 + 提交时间,比如2024KJCY001_申报书_202406121530.pdf。这样归档的时候,文件名本身就是检索条件。

1.3 权限边界模糊,谁都能看所有数据

真实场景里,学校管理员应该能看到所有学院的数据,学院管理员只能看到本院,指导教师只能看到自己名下的项目,学生只能看到自己参与的项目。如果权限不做隔离,这个系统就是摆设。

所以系统采用基于角色的访问控制(RBAC),设计了学生、指导教师、学院管理员、学校管理员四种角色(后面可以扩展评委角色)。前端根据角色动态渲染菜单和按钮,后端在每个接口上做权限拦截,接口层面才是真正的安全边界。

2. 技术选型不是凑热门:SpringBoot、Vue、MyBatis、MySQL各自解决什么问题

选型这块,很多人纠结要不要上微服务、要不要用MyBatis-Plus、前端要不要冲Vue3。我的看法很直接:管理系统这种场景,稳定、好维护、教程多,比什么都重要。

2.1 后端框架:SpringBoot 2.7 + MyBatis,稳定压倒一切

SpringBoot我用的是2.7.x。为什么不用3.x?因为3.x基于Jakarta EE,很多老教程和老项目直接迁移会踩坑,而且2.7还在社区维护期内,资料一大堆,遇到问题搜得到答案。

持久层选了原生MyBatis而不是MyBatis-Plus,一方面是很多课程和毕设要求就是MyBatis,另一方面原生SQL在小团队里语义最清晰。表结构一复杂,连表查询、条件动态拼接,XML里写了什么一眼就能看明白。比如项目列表的分页查询,我要根据角色拼不同的查询条件,用MyBatis的动态SQL能力就很直观:

<select id="selectProjectPage" resultType="com.example.entity.ProjectVO"> SELECT p.*, u.real_name AS creator_name, c.college_name AS college_name FROM project p LEFT JOIN sys_user u ON p.creator_id = u.id LEFT JOIN college c ON u.college_id = c.id <where> <if test="status != null and status != ''"> AND p.status = #{status} </if> <if test="keyword != null and keyword != ''"> AND (p.project_name LIKE CONCAT('%', #{keyword}, '%') OR p.project_number LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="collegeId != null and collegeId != ''"> AND u.college_id = #{collegeId} </if> <if test="teacherId != null and teacherId != ''"> AND p.teacher_id = #{teacherId} </if> </where> ORDER BY p.create_time DESC </select>

这段SQL覆盖了管理员的全局检索、学院管理员的本院过滤、教师的本项目过滤,一个XML标签搞定,可读性比注解拼SQL好太多。

2.2 前端框架:Vue2 + Element UI,对新手最友好

前端选择Vue2,原因很实在:Element UI组件库对管理系统的覆盖度高,表格、表单、弹窗、树形控件开箱即用。Vue3当然更现代,但Composition API、TypeScript、以及Element Plus的适配,对很多刚接触前后端分离的同学来说,学习成本还是偏高。

我的页面结构是这样的:前端通过axios调用后端RESTful接口,全局封装了请求拦截器和响应拦截器。请求拦截器在header里带token,响应拦截器统一处理后端返回的状态码,比如401跳回登录页、500弹出错误提示。每个页面组件里只关注业务本身,数据处理交给封装好的service层。

2.3 数据库版本:MySQL 8.0,注意字符集和时区

MySQL我用的8.0,安装时字符集选utf8mb4,排序规则选utf8mb4_general_ci(对新项目来说utf8mb4_0900_ai_ci也可以)。8.0相比5.7,窗口函数、公共表表达式(CTE)都很好用,后续做统计报表会方便很多。

但有一个坑必须提前说:8.0的默认时区是UTC,连接字符串里不设置serverTimezone的话,Java端拿到的时间会比本地时间少8小时。我的JDBC连接串统一加了serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true,其中allowPublicKeyRetrieval=true是因为MySQL 8.0默认使用caching_sha2_password插件,不设这个参数连接时会报错。

3. 数据库设计:从用户表到项目日志,一张图理清九张核心表

系统的核心业务表我设计成九张:用户表、角色表、用户角色关联表、学院表、项目表、项目成员表、申报材料表、审核日志表、项目状态日志表。下面把每张表的作用和关键字段过一遍。

3.1 用户与组织架构:sys_user、sys_role、sys_user_role、college

用户表是最核心的关联枢纽。字段上除了常规的id、username、password、real_name,我还加了college_id,把用户直接挂到学院下面。密码存储用的是BCrypt加密,不是MD5。MD5撞库太容易了,BCrypt每次加密结果都不一样,安全性好很多。

注意:密码加密只在注册或重置密码时执行一次,登录校验用BCryptPasswordEncoder.matches()对比明文和密文。很多人会忘记在修改密码接口里重新加密,结果改完密码就登录不上了。

用户和角色是多对多关系,所以有中间表sys_user_role。角色不直接落字段到用户表,因为后续很可能加“评委”“答辩组长”这类新角色,多对多扩展起来最灵活。

3.2 项目主表与成员表:project、project_member

项目表是业务核心,字段比较多,重点几个:

  • project_number:项目编号,按规则生成,比如2024-KJCY-001,创建项目时自动生成并唯一
  • project_name:项目名称
  • project_type:项目类型,比如自然科学类、社会科学类、发明制作类
  • teacher_id:指导教师用户ID
  • status:当前状态,0草稿、1待审核、2审核通过、3已驳回、4中期检查中、5待结题、6已结题、7已终止
  • budget:经费预算
  • create_timeupdate_time:创建和更新时间

project_member是项目成员表,存项目ID、学生用户ID、成员角色(负责人/成员)。一个人可以参与多个项目,一个项目有多个人,这是典型的多对多,必须有中间表。

3.3 材料与日志:project_material、audit_log、project_status_log

project_material表存项目的申报书、中期报告、结题报告等材料,字段包括project_id、material_type、file_name、file_url、upload_time。一个项目有多条材料记录,按类型区分。

audit_log表是审核记录,某个项目在某个时间点被哪个角色审核,审核结果是通过还是驳回,意见是什么,全部记录在案。这张表的价值在后期回溯的时候体现得非常明显:哪个环节拖了多久、因为什么驳回,一查就知道。

project_status_log表是状态流转日志。项目进入中期、提交结题、终止,每次状态变更都往这里插一条记录,方便后来人看懂项目的完整生命线。

4. 核心功能实现:登录鉴权、项目申报、文件上传三块硬骨头

4.1 JWT登录鉴权的完整链路

管理系统必须做登录鉴权,我用的是JWT + Spring Security。思路是:用户登录成功后,后端生成一个包含用户ID、用户名、角色列表的token,有效期为24小时。前端把token存在localStorage里,每次请求在axios拦截器里加到header。

后端用过滤器拦截所有/api/**请求,先校验token是否有效,再从token里解析出用户信息,放进ThreadLocal上下文。关键代码大致是这样:

public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); try { Claims claims = JwtUtil.parseToken(token); Long userId = Long.valueOf(claims.get("userId").toString()); // 根据userId查询用户权限,存入SecurityContext UserDetails userDetails = userDetailsService.loadUserById(userId); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (Exception e) { // token解析失败,不放行 } } chain.doFilter(request, response); } }

这里必须提醒一句:JWT只做身份认证,不做权限控制。权限控制靠Spring Security的@PreAuthorize注解,在接口上标注需要的角色:

@PreAuthorize("hasRole('ADMIN')") @GetMapping("/admin/project/list") public Result listAllProject() { ... }

如果只校验token不校验角色,那普通学生登录以后改个接口路径就能看管理员的数据了,这是很多新手项目最常见的安全漏洞。

4.2 项目申报时的并发问题:重复提交和编号唯一

项目申报接口看起来简单,实际上有两个并发坑。

第一个坑是学生重复点击提交按钮,导致同一项目插入两条记录。前端做了按钮禁用是一层防护,但后端也必须兜底。我的处理是在项目创建接口入口加了一个分布式防重逻辑:用Redis的SETNX project_apply_{userId},设置三秒过期,如果获取不到锁直接提示“操作过于频繁”。

第二个坑是项目编号的唯一性。如果多个学生同时提交,A读到了当前最大编号是5,B也读到了5,两个人都插编号为6的记录就会冲突。我直接在project表的project_number字段上建了唯一索引,插入时捕获DuplicateKeyException,捕获后重新生成编号再重试一次。简单粗暴,但非常可靠。

4.3 文件上传:本地存储路径 vs 拦截器映射

文件上传这一块,最省事的方案是文件存本地,数据库只存路径,然后通过Spring Boot的静态资源映射把上传目录暴露出去。我在application.yml里配置:

file: upload-dir: /www/wwwroot/sts/upload/

然后写一个WebMvcConfigurer,把/files/**映射到本地上传目录:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceHandler("file:" + uploadDir); }

这样前端访问http://localhost:8080/files/2024xxx.pdf就能直接预览或下载文件。文件上传接口里做两层校验:扩展名白名单(pdf、doc、docx、zip、jpg、png),大小限制20MB。上传成功后组装文件URL存到project_material表。

注意:生产环境不能把上传目录放在项目jar包同级目录,否则部署时一更新jar包文件就没了。我把它放到一个独立的绝对路径,比如/www/wwwroot/sts/upload,部署目录和存储目录彻底分开。

5. 本地联调与前后端分离开发:配置代理和跨域的正确姿势

前后端分离开发时,最烦的就是跨域。我在前端开发环境用vue.config.js的proxy代理来解决,而不是在后端写@CrossOrigin

5.1 前端代理到底代理了什么

正常情况下,前端跑在8080端口,后端跑在9090端口,浏览器直接请求http://localhost:9090/api会触发跨域报错。用webpack-dev-server的proxy把/api开头的请求转发到后端,这样浏览器看起来所有请求都是发给前端8080的,浏览器层面就不存在跨域了。

vue.config.js配置:

module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:9090', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }

如果你的后端接口本来就是/api/xxx开头,那pathRewrite这行就删掉,别重写路径,否则会404。

5.2 后端还需要处理跨域吗

开发环境有了代理就不用管跨域,但生产环境是Nginx做反向代理,也把前后端统一到同一个域名下,同样没有跨域问题。所以后端可以不配@CrossOrigin。但如果你在本地用npm run dev调试时,直接用Postman或Swagger调试后端接口,那是后端工具直连,也不涉及跨域。

不过我还是在后端留了一个全局的CORS配置,方便某些临时场景,比如学生直接在浏览器地址栏打开某个接口调试。配置如下:

@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

前端fetch请求带cookie时,allowedOriginPattern("*")allowCredentials(true)必须配合使用,不能写死addAllowedOrigin

5.3 联调时接口地址的维护技巧

我习惯把后端的BaseURL统一放在一个配置文件里,开发环境用/api相对路径,生产构建时通过环境变量切换。具体做法是创建.env.development.env.production两个文件:

# .env.development VUE_APP_BASE_URL = '/api' # .env.production VUE_APP_BASE_URL = '/prod-api'

代码里统一用process.env.VUE_APP_BASE_URL + '/project/page'这样的方式拼接。这样换环境只改配置文件,不用全局搜接口路径。

6. 生产环境部署:从零到一跑在公网服务器上

这一章讲纯手动部署的完整流程,重点在Nginx怎么同时托管前端静态页面和后端反向代理。

6.1 服务器环境初始化

我的服务器是CentOS 7.9,内存2G,部署这套系统完全够用。环境准备步骤:

  1. 安装JDK 1.8(或11):yum install java-1.8.0-openjdk
  2. 安装MySQL 8.0:官方Yum仓库安装,或者用Docker跑MySQL实例
  3. 安装Nginx:yum install nginx
  4. 上传项目jar包和前端build产物

前端构建命令:

npm run build

构建完成后,dist目录就是纯静态文件,包含index.html和一堆js/css资源。

6.2 Nginx配置:区分静态资源和动态接口

Nginx配置核心是location块。静态资源直接用alias指向dist目录,接口请求proxy_pass转发到后端SpringBoot的9090端口:

server { listen 80; server_name your_domain.com; # 前端静态资源 location / { root /www/wwwroot/sts/front; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 上传文件访问 location /files/ { alias /www/wwwroot/sts/upload/; } }

这里面有个细节,location /api/后面proxy_pass有两种写法:proxy_pass http://127.0.0.1:9090;proxy_pass http://127.0.0.1:9090/;,有斜杠和没斜杠的路径拼接规则完全不同。想保留/api前缀转发,就不要加末尾斜杠;后端接口本身没有/api前缀,所以我把前端请求路径设为/api,后端接口路径是/project/page,Nginx转发时去掉/api前缀需要在proxy_pass末尾加斜杠,即http://127.0.0.1:9090/,否则后端会收到/api/project/page

6.3 后端jar包启动与守护

nohup启动后端是最简单的:

nohup java -jar sts-system.jar --server.port=9090 > /www/wwwroot/sts/logs/backend.log 2>&1 &

但nohup启动的进程在服务器重启后就没了。更好的方式是用systemd写一个service文件,让系统自己管理后端进程的启停和重启。

/etc/systemd/system/sts.service

[Unit] Description=STS System Backend After=network.target [Service] Type=simple User=root WorkingDirectory=/www/wwwroot/sts ExecStart=/usr/bin/java -Xms256m -Xmx512m -jar /www/wwwroot/sts/sts-system.jar --server.port=9090 Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target

配置好之后执行:

systemctl daemon-reload systemctl enable sts systemctl start sts

写在这里有个好处:日志可以直接用journalctl -u sts -f查看,不用手动维护日志文件。

6.4 如果用了Docker Compose,最简编排方案

如果你更习惯容器化部署,我提供一个最简的docker-compose.yml,包含MySQL8和Java后端两个服务:

version: '3' services: mysql: image: mysql:8.0 container_name: sts-mysql restart: always environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: sts_system TZ: Asia/Shanghai ports: - "3306:3306" volumes: - ./mysql-data:/var/lib/mysql - ./init.sql:/docker-entrypoint-initdb.d/init.sql backend: build: ./backend container_name: sts-backend restart: always depends_on: - mysql ports: - "9090:9090" environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/sts_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: root123

Docker编排的好处是环境一致性好,换服务器迁移的时候一条docker-compose up -d全部搞定。缺点是你得先弄懂镜像、卷、网络这些概念,如果项目比较急,手动部署反而更快。

7. 实操踩坑清单与后续扩展方向

最后把我踩过的坑集中列出来,有的坑花了我一整天排查。

7.1 前端刷新页面404问题

在本地开发时Vue路由用的history模式,刷新子路由没问题,但部署到Nginx后,刷新/project/detail/1就404。原因很简单:Nginx在找不到对应静态文件时直接返回404,而Vue是单页应用,所有路由都应该回到index.html。解决办法就是try_files $uri $uri/ /index.html;这一行,加上就解决了。

7.2 MySQL 8.0的认证插件坑

这个问题在部署时非常典型。后端连不上数据库,报Public Key Retrieval is not allowed。原因前面提过,MySQL 8.0默认认证插件是caching_sha2_password,JDBC驱动需要allowPublicKeyRetrieval=true。这行参数记得加,不加就报错,加了又会有安全性顾虑——所以生产环境更推荐创建专用账号并指定用mysql_native_password插件:

CREATE USER 'sts_user'@'%' IDENTIFIED WITH mysql_native_password BY 'YourStrongPass'; GRANT ALL PRIVILEGES ON sts_system.* TO 'sts_user'@'%'; FLUSH PRIVILEGES;

7.3 文件上传后访问图片404

这个坑一般是StaticResourceMapping的路径没配对。检查三点:upload-dir配置的路径是否正确、addResourceLocations是否以file:开头、Windows下路径的斜杠是/不是\。我最早在Windows本地开发时写成file:E:/upload/,没问题,但换到Linux上一模一样的写法就404,最后发现是Linux路径必须写成file:/www/wwwroot/sts/upload/

7.4 如果还有余力,可以从这几个方向继续扩展

  • 消息通知:项目审核通过或驳回时,站内信通知学生,甚至可以对接邮件
  • 数据可视化:学院立项数量、结题率、项目类型的统计图表,用ECharts做起来很直观
  • Excel批量导入导出:用EasyExcel实现项目列表导出、学生名单批量导入
  • 评审模块:如果做横向课题或竞赛选拔,可以增加评委评分表,支持多评委独立打分后汇总
  • 消息队列:考虑到演示场景和中小型校内使用,本系统没有引入消息队列,如果并发量真的大了,可以考虑用RabbitMQ异步处理通知、日志等非核心链路

我个人做这个系统的体会是:功能不用贪多,但每个功能要做到数据闭环。比如审核操作,前端点了通过,后端不仅要改项目状态,还要写审核日志、发站内信、通知项目负责人,一套动作下来,数据才是完整的。如果你正在做类似系统,先把这九张表的关系吃透,把登录鉴权和项目申请主流程跑通,这个骨架就成了,剩下的事情都是往里面添砖加瓦。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询