Spring Boot+Vue实现教师科研管理系统:申报审批与作业任务一体化
简介教师科研管理系统完整源码与设计资料面向需要完成课程作业或进行系统开发练习的计算机专业学生。系统涵盖科研项目申报、立项、成果录入与统计、资源共享等功能并配有量化评估模块能帮助教师从繁琐事务中解脱同时也为管理者提供科研绩效数据支持。压缩包共九十一个文件大小约一点三八兆字节以C#源代码、界面资源及配置文件为主另含SQL数据库脚本、项目解决方案与两个Word说明文档分别介绍系统源码与数据库设计便于对照理解整体架构与业务流程。源代码工程已覆盖项目申报、成果录入、资源共享及量化评估等核心模块。已有1582人学习下载适合作为课程设计或毕业设计的参考基础也可为快速搭建同类管理系统提供可复用的模块化思路。1. 教师科研管理系统把申报、审批、成果和科研作业归档串成一条线在高校里科研秘书是被报销单、申报书、结题材料和工作量统计反复捶打的人。一个项目从通知申报、教师填表、院系审核到学校审批再到立项后的中期检查、结题和成果登记每一步都靠微信群催、Excel 来回传漏发一个通知就可能让整个申报窗口错过。这套教师科研管理系统把项目申报、审批流转、成果录入、经费归集甚至连教师给学生布置的科研作业/任务都收进同一个平台教师一个入口提交材料科研处一个后台跟踪进度工作量数据自动汇总谁在哪个环节卡壳一眼可见。它适合高校科研处、二级学院科研秘书使用也适合每天要填八个表的教师快速完成填报。2. 技术架构与目录结构Spring Boot 做底、Vue 做面角色和流程分层2.1 技术栈选型前后端分离数据落库缓存热数据这个项目采用了很常见的前后端分离方案。后端用 Spring Boot 2.7 作为基础框架MyBatis-Plus 操作 MySQL 8.0Redis 缓存登录态和热点字典数据文件上传先存本地磁盘生产环境再换成 MinIO 或云存储。前端是 Vue 2 Element UI用 Vuex 管理全局状态axios 统一发起请求。为什么这么选因为高校科研处通常不追求极致的并发性能但要求代码易维护、生态成熟、招人容易这套技术栈在 CRUD 密集型管理系统里几乎是最稳的组合网上资料多二次开发时踩坑成本低。登录认证没有用分布式框架直接在网关或拦截器里解析 JWT token。原因是用例简单校内使用用户量几千不需要引入 Spring Security OAuth2 这种重武器。JWT 无状态后端不存 Session配合 Redis 存 token 黑名单既能控制过期又能支持强制下线。2.2 工程目录前端后端分开结构清晰拿到源码首先看目录结构能快速判断项目入口和模块边界。最终目录如下research-system/ ├── backend/ │ ├── src/main/java/com/wm/research/ │ │ ├── common/ # 统一返回结果、异常处理、常量 │ │ ├── config/ # MyBatis-Plus、CORS、拦截器配置 │ │ ├── controller/ # 对外 REST 接口 │ │ ├── service/ # 业务逻辑层 │ │ ├── mapper/ # MyBatis-Plus 数据访问接口 │ │ ├── entity/ # 数据库表实体 │ │ ├── dto/ # 请求响应封装 │ │ └── utils/ # JWT、文件工具类 │ └── src/main/resources/ │ ├── mapper/ # 手写 XML SQL │ ├── application.yml # 数据源、Redis、文件路径配置 │ └── sql/init.sql # 建库建表脚本 ├── frontend/ │ ├── src/ │ │ ├── api/ # 接口请求封装 │ │ ├── assets/ # 静态资源 │ │ ├── components/ # 通用组件 │ │ ├── router/ # 路由 │ │ ├── store/ # Vuex 模块 │ │ └── views/ # 页面 │ └── package.json └── README.mdbackend 和 frontend 完全独立后端只提供 JSON API前端通过代理转发到后端端口。common 包下的 Result 类统一包装返回结构前端 axios 拦截器统一处理 code 和 message这样新增接口时不用反复写 try/catch。2.3 数据库表设计项目、成果、用户和科研作业任务四类模型系统有四个核心业务域用户权限、科研项目、科研成果、科研作业任务。建表脚本里表名基本都是小写下划线命名。核心表如下表名作用关键字段sys_user用户账号id, username, password, real_name, role_id, dept_idsys_role角色权限id, role_name, role_key, statusproject_info科研项目id, project_no, name, type, leader_id, status, apply_time, file_urlproject_approval项目审批记录id, project_id, approver_id, action, comment, create_timeachievement_info科研成果id, teacher_id, title, type, publish_date, attach_url, scoretask_info科研作业/任务id, teacher_id, title, content, deadline, statustask_submit学生提交记录id, task_id, student_id, submit_file, submit_time, feedbackproject_info 里 status 字段存的是状态机枚举值0 草稿、1 待院系审核、2 待科研处审核、3 已立项、4 被驳回。project_approval 负责把审批链路完整记录下来方便追溯是谁在什么时候批的。task_info 和 task_submit 就是关键词“作业”对应的模块教师发布科研任务或作业学生在线上传文献综述、实验数据等文件系统自动记录提交时间过期后状态变为“已逾期”。2.4 权限模型四种角色对应三条入口系统内置四种角色。管理员拥有全部菜单和接口权限科研处人员能查看全校项目、成果和科研任务学院科研秘书能审核本学院项目可以导出统计表普通教师能申报项目、录入成果、布置作业给名下学生。学生只出现在学科研任务体系里配合教师布置的作业模块。权限控制分成两层前端的菜单渲染按 role 字段动态过滤后端在 Controller 上用自定义 RequetRole 注解拦截非法访问。这种双层控制虽然简单但对内部管理系统完全够用。如果项目要加学院层次只需在 sys_user 里增加 dept_id 字段查询时用 dept_id 过滤即可不需要改太多代码。3. 本地运行从拉代码到浏览器看到登录页3.1 环境准备别用太新的 JDK这个项目依赖较老的 Spring Boot 2.x我用的是 JDK 8 Maven 3.6.3前端用 Node 14.21 和 npm 6.14。如果你本地是 JDK 17启动时大概率会遇到反射访问警告虽然不一定报错但为了减少玄学问题建议装 JDK 8。MySQL 用 8.0 和 5.7 都行Redis 用 6.x 以上版本即可项目没有用到 Redis 的特殊数据结构。具体环境清单JDK 8必须Maven 3.6Node 14 / npm 6MySQL 8.0Redis 6.x启动前确认 MySQL 和 Redis 已经能在本机访问否则项目虽能启动但登录接口会报缓存连接失败。3.2 初始化数据库直接导入 init.sqlbackend/src/main/resources/sql/init.sql 里建好了库和表并内置了测试账号。先创建数据库mysql -uroot -p -e create database research_system default charset utf8mb4; mysql -uroot -p research_system backend/src/main/resources/sql/init.sql然后打开 backend/src/main/resources/application.yml修改数据源配置。关键部分如下spring: datasource: url: jdbc:mysql://localhost:3306/research_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 servlet: multipart: max-file-size: 20MB max-request-size: 100MB custom: upload-path: /data/research-upload jwt-secret: your-secret-keyurl 里必须带 serverTimezone不然 MySQL 8 会报时区错误。upload-path 是上传文件保存的绝对路径Windows 上改成D:/research-upload注意目录要存在否则上传接口直接报“系统找不到指定路径”。3.3 启动后端先 mvn 再确认端口后端是标准 Maven 项目启动方式有两种cd backend mvn spring-boot:run或者先打 jar 再运行mvn clean package -DskipTests java -jar target/research-system-1.0.0.jar --server.port8080我第一次跑的时候直接用mvn spring-boot:run日志停在“Started Application in 12.3 seconds”说明启动成功。后端默认端口 8080如果被占用就去 application.yml 改 server.port。3.4 启动前端npm install 才是第一道坎前端在 frontend 目录下启动命令很常规cd frontend npm install npm run devnpm install 在这类项目里经常卡住如果下载慢就换成淘宝镜像源先拉依赖。启动成功后控制台会提示http://localhost:8081。前端 Dev 服务的端口是 8081接口请求通过 vue.config.js 里的 proxy 转发到 8080跨域问题已经被代理解决。3.5 验证核心链路登录后走一遍申报和作业流程浏览器打开http://localhost:8081用 init.sql 里预设的管理员账号登录。登录后左侧菜单能看到项目管理、成果管理、任务管理、系统管理。可以先用管理员创建一个院系科研秘书账号再去“项目申报”里发起一条新申请上传一份 PDF 附件然后退出用科研秘书账号登录审核走一遍“提交—审核—通过”的流程。紧接着去“科研作业”模块发布一个“提交文献综述”的任务用学生账号登录提交附件。如果这一步通了说明前后端、数据库、文件存储四个环节全部正常。4. 核心模块拆解一个项目申报从进入到返回的完整链路4.1 JWT 登录认证拦截器里做的事后端没有用 Spring Security而是自己写了一个登录过滤器。每次请求都会经过JwtInterceptor它从请求头取 token解析后把用户信息放进 ThreadLocal。核心代码如下public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equals(OPTIONS)) { return true; } String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BizException(401, 未登录); } // 解析 JWT先从 Redis 黑名单判断 token 是否被强制失效 if (redisUtil.hasKey(black: token)) { throw new BizException(401, 登录已过期请重新登录); } JwtUtil.checkToken(token); Long userId JwtUtil.getUserId(token); // 存入 ThreadLocal后续 Controller 直接用 UserContext.set(userId); return true; } }这段代码最容易被忽略的是 OPTIONS 放行。前端跨域请求时会先发一个预检请求如果拦截器在这里不放行ajax 就会报 401。token 解析用 JwtUtil里面包含签名算法和过期时间我一般把 token 有效期设成 2 小时用户活跃时通过接口续期避免太久不操作被踢下线。4.2 项目申报提交Controller 收参数Service 里做校验和落库教师在“项目申报”页面上填完表单后前端调POST /api/project/apply。后端 Controller 只做参数接收业务处理都在 ServicePostMapping(/apply) public Result? apply(Valid RequestBody ProjectApplyRequest req) { // 校验同一教师不能同时申报两个同类型项目 long count projectService.lambdaQuery() .eq(ProjectInfo::getLeaderId, UserContext.getUserId()) .eq(ProjectInfo::getType, req.getType()) .in(ProjectInfo::getStatus, 0, 1, 2, 3) .count(); if (count 0) { return Result.fail(已有相同类型的在途项目不能重复申报); } ProjectInfo info new ProjectInfo(); BeanUtils.copyProperties(req, info); info.setStatus(ProjectStatus.DRAFT.getValue()); info.setApplyTime(LocalDateTime.now()); projectService.save(info); return Result.ok(info.getId()); }这里有一个很实际的设计状态为草稿的项目保存后不直接进入审核流要等教师点“提交”按钮才会把 status 从 0 改成 1。因为教师填申报书往往不是一口气完成的中途保存一次退出再回来很常见。ProjectApplyRequest里包含 projectName、projectType、funds、summary、attachmentUrls 等字段前端传入的文件是先用另一个上传接口拿到的 URL这里只存路径字符串。注意校验逻辑是查“在途项目”已驳回的可以重新编辑提交不算在途。4.3 审批流程状态机每个操作都写一张流水表审批是项目管理系统里最容易踩坑的地方。这个实现没有引入 Activiti 工作流而是用一个状态字段叠加审批记录表。审批接口接收 action 参数0 表示驳回1 表示通过Transactional public void approve(Long projectId, Integer action, String comment) { ProjectInfo info projectService.getById(projectId); Integer curStatus info.getStatus(); int nextStatus; if (action 0) { nextStatus ProjectStatus.REJECTED.getValue(); } else if (curStatus ProjectStatus.DEPT_CHECK.getValue()) { nextStatus ProjectStatus.SCHOOL_CHECK.getValue(); } else if (curStatus ProjectStatus.SCHOOL_CHECK.getValue()) { nextStatus ProjectStatus.APPROVED.getValue(); } else { throw new BizException(当前状态不可审批); } // 更新项目状态 projectService.lambdaUpdate() .eq(ProjectInfo::getId, projectId) .set(ProjectInfo::getStatus, nextStatus) .set(ProjectInfo::getUpdateTime, LocalDateTime.now()) .update(); // 写入审批流水 ProjectApproval approval new ProjectApproval(); approval.setProjectId(projectId); approval.setApproverId(UserContext.getUserId()); approval.setAction(action); approval.setComment(comment); approval.setCreateTime(LocalDateTime.now()); approvalService.save(approval); }这里的关键点是Transactional状态更新和审批流水必须成对成功或成对失败否则会出现项目状态已经变成已立项但审批记录缺失的脏数据。我踩过这坑后来统一在审批 Service 方法上加了事务涉及状态变化的操作全部走一个入口不允许 Controller 里直接改状态。4.4 科研作业任务提交教师发布、学生上传、逾期自动标记关键词“作业”在这个系统体现为 task_info 和 task_submit 两张表联动的功能。教师端发布任务PostMapping(/api/task/publish) public Result? publish(RequestBody TaskPublishRequest req) { TaskInfo task new TaskInfo(); task.setTitle(req.getTitle()); task.setContent(req.getContent()); task.setTeacherId(UserContext.getUserId()); task.setDeadline(req.getDeadline()); task.setStatus(1); taskService.save(task); return Result.ok(); }学生端通过任务 ID 提交作业提交接口接收文件路径和任务 ID如果提交时间超过 deadline系统仍然接受文件但 status 会自动标记为 2逾期PostMapping(/api/task/submit) public Result? submit(RequestParam Long taskId, RequestParam String fileUrl, RequestParam(required false) String remark) { TaskInfo task taskService.getById(taskId); TaskSubmit submit new TaskSubmit(); submit.setTaskId(taskId); submit.setStudentId(UserContext.getUserId()); submit.setSubmitFile(fileUrl); submit.setRemark(remark); submit.setSubmitTime(LocalDateTime.now()); if (task.getDeadline().isBefore(LocalDateTime.now())) { submit.setStatus(2); } else { submit.setStatus(1); } taskSubmitService.save(submit); return Result.ok(); }这里的参数 fileUrl 是之前上传接口返回的路径比如/upload/2025/xx.pdf。前端预览时直接拼接文件访问前缀后端拦截/upload/**路径映射到本地磁盘目录。如果想让任务支持多次提交可以在 submit 表加 version 字段不过当前版本是“一任务一人一条”的简单约束。4.5 成果统计报表一条连表查询汇总工作量科研处最关心的汇总统计是用教师成果数量计算科研工作量。这条统计 SQL 写在 XML 里根据成果类型和年份分组输出每位老师的论文、项目、专利数量以及折算分值select idcountWorkload resultTypejava.util.Map SELECT u.real_name AS teacherName, YEAAR(a.publish_date) AS year, a.type AS achievementType, COUNT(*) AS cnt, SUM(case a.type WHEN paper THEN 10 WHEN patent THEN 15 ELSE 5 END) AS totalScore FROM achievement_info a INNER JOIN sys_user u ON a.teacher_id u.id WHERE YEAR(a.publish_date) #{year} GROUP BY u.real_name, YEAR(a.publish_date), a.type ORDER BY totalScore DESC /select这段统计 SQL 没有把计算逻辑写死在 Java 里而是用数据库的 CASE WHEN 完成分值折算。如果你的学校分值规则复杂比如一区论文和二区论文分别 20 分和 10 分可以增加一个 achievement_score_type 字段折算系数的变化就不需要改 SQL 了。5. 常见问题与避坑启动失败、跨域、上传文件和缓存黑盒5.1 MySQL 连接报时区错误启动直接失败现象后端启动时提示The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。原因MySQL 8.0 默认时区设置不受 MyBatis 控制连接字符串没设置 serverTimezone。解决在 application.yml 的 jdbc url 末尾加上serverTimezoneAsia/Shanghai同时建数据库时用 utf8mb4否则中文注释和名称会变乱码。5.2 前端登录接口 401跨域预检请求被拦截器挡住了现象前端登录接口一直报 401浏览器控制台出现OPTIONS http://localhost:8080/api/user/login。原因axios 请求带Content-Type: application/json会先发 OPTIONS 预检而 JwtInterceptor 里对所有非 OPTIONS 请求强制要 token预检请求直接失败。解决在拦截器最前面加一行if (request.getMethod().equals(OPTIONS)) return true;同时在 WebMvcConfig 里配置允许所有源和所有请求头。这个坑很隐蔽因为有时本地用 vue.config.js 代理不会复现直接地址跨域请求时才出现。5.3 上传文件访问 404路径映射没配置静态资源处理器现象上传接口返回了/upload/2025/xxx.pdf但前端 img 标签访问时 404。原因upload-path 指向磁盘/data/research-upload后端只保存了文件没有把/upload/**映射到磁盘目录。解决在 WebMvcConfigurer 里配置addResourceHandlers(/upload/**).addResourceLocations(file: customUploadPath /)。注意 Windows 路径末尾要带/否则拼接后少一层目录。这个问题在 Linux 服务器上最容易翻车因为路径大小写也有影响。5.4 用户权限改了但界面没变Redis 缓存了角色菜单现象管理员把普通教师的角色改成科研秘书后该用户还是看不到新增菜单。原因登录时把菜单列表和角色信息放进了 Rediskey 是 userId角色变更后没有同步清理。解决角色变更时主动删除该用户的 Redis 缓存或者在读取菜单时不缓存每次都从数据库查。这个坑属于典型的缓存一致性我建议在用户管理 Service 的 updateRole 方法里调用redisUtil.delete(menu: userId)避免统计时发现权限还是旧的。5.5 审批重复提交导致状态翻转前端防重没做好现象教师提交项目申报后快速点击两次“提交”按钮后台收到两个请求状态被连续变更一条项目直接跳到科研处审核。原因前端没有防重复提交后端状态机里每一次动作都无条件推进。解决在审批方法里先判断当前状态是否符合预期再用条件更新setStatus(nextStatus).eq(ProjectInfo::getStatus, curStatus)如果更新行数为 0 就说明被别人抢先处理返回“请勿重复操作”。另一种做法是在 apply 接口生成唯一业务流水号靠数据库唯一索引挡重复但不如状态条件更新直观。6. 二次开发从改状态机到接企业微信通知我常用的改造路径这项目留给开发者的修改空间很大我一般从三个方向切审批流程调整、数据导出、消息提醒。首先要改的是审批层级。很多学校要求学院科研秘书之外还得有主管院长审批。改法很简单在 status 枚举里多加一个DEPT_LEADER_CHECK状态然后在 approve 方法的状态机分支里插入一步把DEPT_CHECK的下一个状态指向DEPT_LEADER_CHECK再把DEPT_LEADER_CHECK的下一个状态指向SCHOOL_CHECK。审批流水表不用改因为 project_approval 已经能记录多轮审批记录。前端审批页面只根据当前 status 显示按钮不需要大改。第二件常做的事是导出 Excel。科研处几乎每个月底都要统计项目汇总表我直接在现有代码里引入 EasyExcel写一个导出接口GetMapping(/api/project/export) public void export(HttpServletResponse response) throws IOException { ListProjectInfo list projectService.list(); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenameproject.xlsx); EasyExcel.write(response.getOutputStream(), ProjectInfo.class).sheet(项目列表).doWrite(list); }导出接口和列表查询共用数据源但要注意把select *改成只查需要的列否则大表导出时内存占用非常难看。第三件是接入企业微信通知。高校科研处喜欢在项目状态变化时给教师推一条消息。实现上不必引入消息中间件直接在审批成功后调企业微信 webhook 接口restTemplate.postForObject(webhookUrl, Map.of(msgtype, text, text, Map.of(content, 您的项目已通过审核)), String.class);webhook 配置放到 application.yml前端不用动。消息推送失败不能影响主流程用 try/catch 包住即可。如果要做得更稳把通知记录丢进 task_queue 表后台定时任务重新投递但这对于内部系统属于过度设计了。改完代码后我最推荐做的是回归“项目申报—审批—作业提交”三条核心链路。用 Postman 直接调接口重点看状态机是否按照新流程走上传文件是否还能正常预览。这个项目最让我放心的一点是状态字段和审批流水分离改流程只要动状态机分支不容易把历史数据搞乱。从那以后我每次改完审批逻辑都强制走一遍两轮审批回归养成习惯了。希望这些实践能帮你少走几步弯路。本文还有配套的精品资源点击获取