资讯详情

基于Spring Boot与Vue的科研管理系统:状态机驱动流程与计分策略设计

📅 2026/10/17 7:27:15 | 华诺云谱 👁 阅读
基于Spring Boot与Vue的科研管理系统:状态机驱动流程与计分策略设计
简介教师科研管理系统是一套面向教育信息化领域的完整项目源码适合高校教师、科研管理人员以及需要完成课程设计或毕业设计的学生学习和参考。该系统围绕科研项目申报、立项执行、成果登记、资源共享与量化评估等环节设计能够有效减少科研管理中的重复事务提升信息同步效率。压缩包共91个文件大小约1.38MB主体为C#源代码同时包含窗体资源、项目配置、数据库脚本以及Word版源代码说明和数据库设计文档另有可直接运行的程序及运行库便于快速体验系统功能。已有1582人学习下载。通过源码与文档对照阅读读者可以了解系统分层结构、数据库表关系、核心功能模块的实现方式并掌握从需求分析、界面设计到编码发布的完整流程其中个性化定制、数据加密、与其他系统集成等设计思路也可迁移到相似管理系统的开发中。1. 教师科研管理系统从 Excel 台账到状态机驱动的科研全流程教师科研管理系统最真实的场景是在学期末的科研处某高校的管理员对着三张互相对不上的 Excel 表花了两个通宵才把全院教师的工作量核算完公示之后还有老师拿着 PDF 质疑某篇论文漏统计只能再翻工一轮。这个项目拆出来正是要替代手工台账教师在线填报论文、项目、专利和获奖科研处在线审核系统按年度、院系、职称自动折算工作量最终汇总成可导出的报表。适合两类人看一是想把这套源码直接改造成本校流程的后端工程师二是正在设计科研管理类系统、想参考状态流转与计分公式怎么落地的架构师。2. 整体架构与技术选型Spring Boot Vue 前后端分离的分层设计科研管理系统表面上是一堆增删改查真正容易被低估的是状态流转和计分规则这两块。一个课题从申报到结题要经历七八个状态一篇论文的计分受类型和作者位次双重影响所以代码不能只按“Controller-Service-Mapper”三层一糊了之而是要在业务层内部再做拆分。2.1 技术栈盘点后端选型与前端选型的理由后端选用 Spring Boot 2.7 搭配 MyBatis-Plus 3.5数据库是 MySQL 8.0前端是 Vue 3 Vite Element Plus报表用 EChartsExcel 导入导出用 EasyExcel。登录认证没有用 Spring Security而是选了 Sa-Token原因是管理端场景只需要登录校验、权限注解、踢人下线Sa-Token 几个注解就能闭环不必引入一整套 Security 过滤链。层次选型说明后端框架Spring Boot 2.7稳定版本社区资料多ORMMyBatis-Plus 3.5单表 CRUD 免写 SQL复杂统计走自定义 Mapper数据库MySQL 8.0支持窗口函数统计报表更好写认证授权Sa-Token注解鉴权配置量小导入导出EasyExcel基于 SAX 逐行写内存占用低于 POI前端Vue 3 Element Plus表单密集场景组件成熟ORM 选 MyBatis-Plus 而不是 JPA主要是考虑查询条件组合多工作量统计要按院系、年度、职称、成果类型多维度过滤MP 的 LambdaQueryWrapper 拼动态条件比 JPA 的 Specification 直观团队成员接手成本也低。文件存储先落本地磁盘application.yml 里预留了 minio.endpoint 参数后续并发上来切换对象存储时只需改 FileStorageService 一个接口的实现业务层无感知。2.2 后端目录结构一个请求从入口到 Mapper 的落点src/main/java/com/edu/research ├── controller │ ├── ProjectController.java │ ├── PaperController.java │ ├── WorkloadController.java │ └── ReportController.java ├── service │ ├── project │ │ ├── ProjectService.java │ │ ├── ProjectServiceImpl.java │ │ └── ProjectStateMachine.java │ ├── workload │ │ ├── WorkloadCalculator.java │ │ ├── WorkloadContext.java │ │ ├── PaperWorkloadStrategy.java │ │ └── ProjectWorkloadStrategy.java │ ├── file │ └── report ├── mapper ├── entity ├── dto ├── common └── config ├── SaTokenConfig.java └── WebMvcConfig.java以教师提交项目申报为例请求路径是 POST /api/project/apply先落到 ProjectController.apply()再进 ProjectServiceImpl.apply()状态机在这里参与校验最后由 ProjectMapper.insert() 落库。Controller 只做参数接收和路由业务规则全在 Service 层核心的状态校验从 Service 里抽出来单独放在 ProjectStateMachine避免状态流转逻辑散落在各个业务方法里。controller 包里按业务域拆分而不是按角色拆分。ProjectController、PaperController 同时服务教师端和管理端对外通过 Sa-Token 的 SaCheckPermission 注解区分接口访问权限。dto 包和 entity 包刻意分离entity 映射数据库字段dto 承接前端请求参数和校验注解避免把 NotBlank 这类约束直接写在实体上防止同一字段被多个接口复用后校验规则互相打架。RestController RequestMapping(/api/project) public class ProjectController { Resource private ProjectService projectService; PostMapping(/apply) public ResultLong apply(RequestBody Valid ProjectApplyDTO dto) { return Result.ok(projectService.apply(dto)); } GetMapping(/page) public ResultPageResultProjectVO page(ProjectQueryDTO query) { return Result.ok(projectService.page(query)); } }Valid 注解触发 DTO 上的字段校验比如 projectName 的 NotBlank、fund 的 DecimalMin。apply 方法返回 Long 类型主键 ID前端拿到 ID 后跳转详情页。统一返回体 Result 的 code 为 0 表示成功非 0 时全局异常处理器会把 BizException 中的 message 带回页面提示。2.3 数据模型教师、项目、论文三张核心表的关系CREATE TABLE teacher ( id bigint NOT NULL AUTO_INCREMENT, teacher_no varchar(20) NOT NULL COMMENT 工号, name varchar(50) NOT NULL COMMENT 姓名, college varchar(100) NOT NULL COMMENT 所属院系, title varchar(20) DEFAULT NULL COMMENT 职称教授/副教授/讲师, created_at datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_teacher_no (teacher_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT教师基本信息表; CREATE TABLE project ( id bigint NOT NULL AUTO_INCREMENT, teacher_id bigint NOT NULL COMMENT 负责人教师ID, project_no varchar(50) DEFAULT NULL COMMENT 立项编号, project_name varchar(200) NOT NULL COMMENT 项目名称, project_type varchar(20) DEFAULT NULL COMMENT 国家级/省部级/市厅级/校级, fund decimal(10,2) DEFAULT 0.00 COMMENT 到账经费, status varchar(20) NOT NULL COMMENT 当前状态, apply_time datetime DEFAULT NULL, approve_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_teacher_id (teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT科研项目表; CREATE TABLE paper ( id bigint NOT NULL AUTO_INCREMENT, teacher_id bigint NOT NULL COMMENT 作者教师ID, paper_title varchar(300) NOT NULL, journal varchar(200) DEFAULT NULL COMMENT 期刊名称, paper_type varchar(20) DEFAULT NULL COMMENT SCI/EI/CORE/普通, publish_year int DEFAULT NULL, file_path varchar(300) DEFAULT NULL COMMENT 论文附件路径, status varchar(20) DEFAULT NULL, PRIMARY KEY (id), KEY idx_teacher_id (teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT论文成果表;教师表与项目表、论文表都是 1 对 N 关系没有建中间表因为一个项目的负责人只有一个填报时虽然可能涉及团队多个成员但成员贡献拆分放在工作量记录表里处理项目主体只挂负责人。工作量表单独设计携带 teacher_id、year、biz_type、total_score 四个核心字段这样一次填报可以按多个年度分别折算统计时只要按 year 过滤。状态字段 status 用 varchar 存字符串而不是数字值如 DRAFT、APPLYING、APPROVED排错时看数据库就能读出动线不需要再查字典表。合法流转的路径定义放在代码状态机里而不是数据库原因在下一章展开。3. 核心模块拆解项目申报、工作量折算与待办提醒的完整链路3.1 项目申报与状态机从草稿到结题的流转控制一个课题的完整生命周期是草稿、申报、审核、立项、在研、待验收、已结题中间任意一步都可能被驳回。如果这些状态流转写成 if/else 散落在 Service 里两星期后自己都容易改乱。这个项目把状态流转单独抽成一个状态机类用可达矩阵集中管理。public enum ProjectState { DRAFT, APPLYING, APPROVED, REJECTED, IN_PROGRESS, PENDING_ACCEPTANCE, ACCEPTED, CLOSED; private static final MapProjectState, ListProjectState TRANSITIONS new HashMap(); static { TRANSITIONS.put(DRAFT, Arrays.asList(APPLYING)); TRANSITIONS.put(APPLYING, Arrays.asList(APPROVED, REJECTED)); TRANSITIONS.put(APPROVED, Arrays.asList(IN_PROGRESS)); TRANSITIONS.put(IN_PROGRESS, Arrays.asList(PENDING_ACCEPTANCE, CLOSED)); TRANSITIONS.put(PENDING_ACCEPTANCE, Arrays.asList(ACCEPTED, REJECTED)); TRANSITIONS.put(ACCEPTED, Arrays.asList(CLOSED)); } public boolean canTransitionTo(ProjectState target) { return TRANSITIONS.getOrDefault(this, Collections.emptyList()).contains(target); } }canTransitionTo 把可流转路径收敛到一张静态表里新增状态时只改这一处。比如将来要加一个退回修改状态只要在 DRAFT 的可达列表里加入对应枚举即可。状态机的调用方是 ProjectStateMachine.transition() 方法它负责校验、落库和写日志三个动作Service public class ProjectStateMachine { Resource private ProjectMapper projectMapper; Resource private ProjectLogService projectLogService; public void transition(Project project, ProjectState target, String operator, String remark) { if (!project.getStatus().canTransitionTo(target)) { throw new BizException(非法状态流转 project.getStatus() - target); } ProjectState from project.getStatus(); project.setStatus(target); projectMapper.updateById(project); projectLogService.log(project.getId(), operator, from, target, remark); } }这段逻辑把非法流转挡在业务开头任何绕过状态机的接口调用都会在第一步失败。operator 参数记录操作人工号remark 是审批意见这两个字段会完整写进 project_log 表。科研项目的审核记录在结题验收时要求留痕审计需要看到谁在什么时间把项目从申报改成了驳回所以日志不能只在业务表里更新一个 status 字段了事。transition 方法不直接加事务注解由调用方 ProjectServiceImpl 控制事务粒度。这样设计是为了应对批量验收场景一次性要流转多个项目状态事务应当包住整个循环而不是让单条状态变更自成一个事务。3.2 工作量折算策略模式下的三类计分规则工作量折算是各高校差异最大的业务点。论文按类型给分SCI 一篇 10 分EI 6 分核心 4 分普刊 1 分通讯作者再加成 20%项目按级别和到账经费折算专利和获奖又是另一套规则。这些计分规则如果都堆在 Service 里新增一个成果类型就要改多个方法。项目里用策略模式把计分规则拆开。Data public class WorkloadContext { private Long teacherId; private String year; private String bizType; // PAPER / PROJECT / PATENT / AWARD private Paper paper; private Project project; private String authorRank; // 第一作者 / 通讯 / 参与 }WorkloadContext 是策略模式的入参载体bizType 决定选择哪套策略paper 和 project 按类型二选一传入。策略接口和论文计分实现如下public interface WorkloadStrategy { String getType(); BigDecimal calculate(WorkloadContext ctx); } Component public class PaperWorkloadStrategy implements WorkloadStrategy { Override public String getType() { return PAPER; } Override public BigDecimal calculate(WorkloadContext ctx) { if (ctx.getPaper() null) { throw new BizException(论文折算缺少 paper 参数); } BigDecimal base switch (ctx.getPaper().getPaperType()) { case SCI - new BigDecimal(10); case EI - new BigDecimal(6); case CORE - new BigDecimal(4); default - new BigDecimal(1); }; if (通讯.equals(ctx.getAuthorRank())) { return base.multiply(new BigDecimal(1.2)); } return base; } }switch 表达式在 Java 14 以后可用这里把论文类型到基础分的映射写得非常直观。通讯作者返回 score * 1.2第一作者不做加成。注意 authorRank 的取值在前端下拉框做了约束后端用 equals 而不是 避免字符串常量池比较的隐患。策略的注册与调度由 WorkloadCalculator 完成Service public class WorkloadCalculator { private final MapString, WorkloadStrategy strategyMap; public WorkloadCalculator(ListWorkloadStrategy strategies) { this.strategyMap strategies.stream() .collect(Collectors.toMap(WorkloadStrategy::getType, s - s)); } public BigDecimal calculate(WorkloadContext ctx) { WorkloadStrategy strategy strategyMap.get(ctx.getBizType()); if (strategy null) { throw new BizException(未找到计分规则 ctx.getBizType()); } return strategy.calculate(ctx); } }Spring 启动时会把所有实现了 WorkloadStrategy 接口的 Bean 收集到 List 中构造器注入后转成 Map。新增专利计分规则时只要新写一个 PatentWorkloadStrategy 类并标注 Component无需改动 Calculator。后面做二次开发把分数改成数据库配置时策略接口保持不变只需要把 base 分从枚举或 switch 改成查字典表。3.3 统计报表多维度聚合与 EasyExcel 导出报表需求最集中的是按院系、职称、年度三个维度汇总工作量。工作量表提前冗余了 year 字段报表 SQL 只需 JOIN 一次教师表即可取到院系和职称SELECT t.college AS college, t.title AS title, SUM(w.total_score) AS total_score FROM workload w JOIN teacher t ON w.teacher_id t.id WHERE w.year #{year} GROUP BY t.college, t.title ORDER BY t.college, t.title;这条 SQL 落在 workloadMapper.sumByCollegeAndTitle 方法上返回值用 ListMapString, Object 接收省去为报表建专门 VO。报表数据不需要强类型约束前端拿到 rows 后直接绑定到表格组件即可。导出则用 EasyExcel 完成GetMapping(/export) public void export(HttpServletResponse response, RequestParam Integer year) throws IOException { ListWorkloadSummaryVO list workloadMapper.sumByCollegeAndTitle(year); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment;filenameworkload_ year .xlsx); EasyExcel.write(response.getOutputStream(), WorkloadSummaryVO.class) .sheet(工作量汇总) .doWrite(list); }这里导出几千行数据用 POI 的 XSSFWorkbook 会吃很多内存EasyExcel 采用 SAX 模式逐行写内存峰值要低一个量级。Content-Disposition 里文件名用了 ASCII 形式的 workload_year.xlsx避免中文文件名在不同浏览器下的编码差异。3.4 待办提醒把年度科研任务做成教师作业清单教师活跃度往往靠待办提醒来支撑。科研处每年年初给教师生成年度科研任务比如本年度必须提交一篇论文或参与一个在研项目系统把它当作一份作业下发到教师端。实现上是一个 Spring 定时任务每天扫描即将到期的未完成作业并推送提醒Component public class TaskRemindScheduler { Resource private TaskMapper taskMapper; Resource private MessageSender messageSender; Scheduled(cron 0 0 9 * * ?) public void remind() { ListTaskVO tasks taskMapper.findPendingDeadline(15); for (TaskVO task : tasks) { messageSender.send(task.getTeacherId(), 您有一份科研作业《 task.getTitle() 》即将到期请及时提交。); } } }cron 表达式表示每天上午 9 点执行。findPendingDeadline 参数 15 表示扫描 15 天内到期且状态为待办的作业这个天数抽成了配置项避免改动时重新部署定时任务类。MessageSender 接口有两个实现站内信和邮件默认走站内信邮件实现逻辑留着备用教师端的作业清单页面轮询站内信接口即可做到半小时内感知。4. 部署与避坑从建库到跑通全流程的实战记录4.1 本地启动步骤建库、配置、前端代理拿到源码后在本地跑通核心是三步。第一步初始化数据库mysql -uroot -p docs/sql/init.sqlinit.sql 包含建库、建表、字典数据三部分默认数据库名 edu_research。然后修改后端配置# application.yml 中修改数据源 spring: datasource: url: jdbc:mysql://localhost:3306/edu_research?serverTimezoneAsia/Shanghai username: root password: 你的密码之后启动后端和前端mvn spring-boot:run cd frontend npm install npm run dev后端占 8080 端口前端 Vite 默认 5173。前端请求 /api 前缀的接口会由 vite.config.js 里的 proxy 代理转发到 8080开发阶段不需要处理跨域。登录时使用初始化脚本里预置的管理员账号或者用脚本生成的演示教师账号。4.2 五条踩坑记录与排查思路避坑一数据库时间相差 8 小时现象项目申报时间在管理端显示比当前时间晚 8 小时。原因JDBC 连接 URL 没有指定 serverTimezone驱动默认按 GMT 时区处理。解决连接串末尾加上 serverTimezoneAsia/Shanghai同时建表时间字段统一用 datetime 类型。避坑二项目列表加载缓慢现象带条件查询的状态列表三秒钟才渲染出数据。原因列表查询把备注、附件路径等大字段一起 SELECT 出来还参与了排序导致临时表开销大。解决列表查询返回 DTO只取表格展示需要的字段详情大字段单独走详情接口查询 SQL 里去掉 order by 大字段的排序。避坑三前端菜单权限形同虚设现象教师账号刷新页面后直接输入管理端 URL 能打开页面。原因菜单权限只在前端路由守卫里做了判断后端接口没有加权限注解绕过前端页面直接请求接口仍然放行。解决后端所有管理接口统一加 SaCheckPermission 注解前端路由守卫只负责隐藏菜单和拦截跳转真正权限判断放在服务端。避坑四EasyExcel 导入中文乱码现象教师名单批量导入后姓名显示乱码。原因模板文件本身不是 UTF-8 编码或者带 BOM 头被 EasyExcel 误读。解决模板用 Excel 另存为 CSV 时选 UTF-8Java 侧读取时用 InputStreamReader 转码先做 BOM 检测再交给 EasyExcel。避坑五批量验收接口偶发栈溢出现象年终批量结题验收时接口偶尔抛 StackOverflowError。原因状态机定义时让 A 状态可流转到 BB 又可流转到 A批量循环里两个项目互相触发流转死循环直到栈溢出。解决状态机定义严格单向流转DRAFT 只能走向 APPLYING不可以反向撤回需要退回修改的场景走独立接口不经过状态机自动迁移。提示排查这类问题先看日志栈顶如果是状态机相关类就检查枚举里的 TRANSITIONS 是否有环路。5. 二次开发思路把折算公式和审批链做成配置5.1 改造点一将计分公式搬进字典表现在的工作量分数写在策略类的 switch 表达式里。高校之间的计分差异很大直接改源码再重新打包部署周期太长。落地做法是新增 sys_dict_item 表key 为 WORKLOAD_PAPER_TYPEvalue 为对应分数。PaperWorkloadStrategy 里改成通过 DictService 查字典查不到再回退到默认分BigDecimal base dictService.getValue(WORKLOAD_PAPER_TYPE, ctx.getPaper().getPaperType()) .map(BigDecimal::new) .orElse(new BigDecimal(1));这样科研处管理员可以自己在系统里维护计分标准不需要开发介入。策略接口和 WorkloadCalculator 的调度逻辑保持不变。5.2 改造点二审批流水可追溯目前状态机日志只记录每次状态变更的起点和终点。二次开发时增加 audit_flow 表把同一业务线的连续审批串成一条流水字段包括业务类型、业务 ID、审批人顺序、操作时间、审批意见。结题验收时可以一次展示整条审批链教师端也能看到项目从申报到结题的完整轨迹。5.3 验证用例覆盖正常路径与驳回边界改造完成后先用构造数据跑一遍验证清单SCI 论文第一作者应折算 10 分EI 通讯作者应折算 7.2 分校级项目按 2 分折算这三组用例用来验证公式修改没有回退。边界用例放在驳回与撤回上确保项目被驳回后教师可以重新编辑再提交并且日志中能看到两次提交的时间差和审批人变化。这套系统拆完给我最大的教训是状态流转和计分规则这两块永远是先梳理清楚再写代码直接开写 CRUD 后面全是返工。从那以后我每次接手类似的管理系统第一步一定是画状态流转路径和枚举规则把所有非法路径先列出来再动手写第一行业务代码。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑