ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SSM框架实现高校科研申报管理系统:状态机与数据库设计全解析

SSM框架实现高校科研申报管理系统:状态机与数据库设计全解析 做了这么多年Java项目也带过不少学生做毕业设计我发现高校科研申报管理系统这类题目在计算机毕设里出现频率极高。原因很简单业务不复杂但足够完整管理角色多、流程状态多、数据关系清晰非常适合用来展示SSM框架的整合能力。这个题目如果只做到增删改查答辩时基本会被按在地上摩擦。因为你没有把申报全流程这个核心表达出来。本文我把这个系统从业务拆解、数据库设计到代码实现、后期避坑完整走一遍希望你能把一个看起来平平无奇的毕设题做出一套有思考深度的项目。1. 项目业务拆解科研申报这件事到底复杂在哪1.1 先搞懂业务再写代码高校科研申报的基本流程一个老师要做课题大概要经历这几个环节填写申报书、准备支撑材料、提交到院系、院系科研秘书初审、科研处复审、组织专家评审、立项公示、签订任务书。就这么一段话拆开来看里面藏着几个关键的痛点申报书的状态是模糊的。老师提交了但到底到哪个环节了有没有被驳回是学院驳回还是科研处驳回了没有系统之前全靠打电话问谁都不掌握准确信息。材料是散落的。申报书、查新报告、可行性报告、成果附件这些文件可能在学院桌面上、在老师邮箱里、在科研处U盘里一旦要归档找东西找到崩溃。审批过程没法留痕。谁审的、什么时候审的、意见是什么纸质时代不容易追溯。一旦出现争议没有任何流程证据。统计极其痛苦。学院想统计本院今年申报了多少课题、通过率多少就得把Excel翻来翻去。所以这个系统的核心职责不是录数据而是三件事管住申报数据、串起审批流程、留好过程痕迹。在答辩的时候如果你能先把这几句话讲明白评委基本就能判断你是真做过需求分析的而不是拿到题目就开始建表写代码。1.2 角色权限模型五种身份如何划清边界系统的用户角色我一共设计了五个这个数不多不少刚好能体现多角色系统的权限设计能力又不至于把项目拖得太大角色核心操作权限边界系统管理员用户管理、角色分配、数据字典维护不参与具体业务审批科研人员教师申报书填报、修改、提交、查看进度只能看到自己的申报项目学院科研秘书本院项目初审、退回、提交复审只能看到本学院项目科研处管理员复审、组织评审、立项管理全流程可见评审专家在线打分、填写评审意见只能看到分配给自己的项目这里有一个设计要点项目表上必须有一个college_id字段用来做数据隔离而不是在用户表上做判断。原因是后期所有按学院维度的统计都依赖于项目表的这个字段把它放在用户表上会让SQL多好几次关联查询得不偿失。1.3 核心用例梳理除了角色权限还有几个用例必须提前想清楚否则后面模块容易打架申报人提交后能不能撤回我最终设计的方案是只有待初审状态下允许撤回一旦学院科研秘书点击了受理申报人就只能等结果。这个规则一定要写清楚因为在JSP页面上要据此判断撤回按钮是否显示。驳回后能不能改能但只能改被驳回的那一版。所以需要驳回后状态回退到草稿的逻辑。专家评审能不能看到申报人信息为了避免人情分正常做法是匿名评审但毕设阶段做成实名也说得过去。如果你想出彩可以在评审表上做一个is_anonymous字段用MyBatis的if标签动态控制查询条件。这个环节梳理完之后数据库的表结构基本就定型了业务字段该有哪些状态该怎么流转心里都有数了。2. 技术选型为什么毕设题目写SSM我就老老实实用SSM2.1 SSM三个组件各管什么使用SSM框架是很多高校Java课程的基本盘。Spring负责对象管理和事务SpringMVC负责请求分发和参数绑定MyBatis负责数据库访问。只要你清楚这三层的职责边界写出来的代码就会很清爽。有的同学会问今年Spring Boot才是主流题目里写SSM框架我能不能擅自换成Spring Boot我建议不要。原因有三点毕设题目通常是开题时定好的评委会对照题目检查工作量。你换框架等于自己把题目改成了Spring Boot MyBatis高校科研申报系统答辩时容易被追问为什么脱离题目。SSM的整合过程本身就包含大量的配置工作web.xml、spring-context.xml、spring-mvc.xml、mybatis-config.xml、数据库连接池、扫描器配置。这些配置折腾明白你对框架的理解会深一层答辩时也更有内容可以讲。Spring Boot的使用难度确实低但也就意味着你要额外解释自动配置的原理否则评委可能会觉得你是照抄别人项目。当然如果你开题时写的本来就是基于Spring Boot的科研申报系统那技术栈完全可以用Spring Boot。但如果题目明确是SSM就按SSM做别给自己找麻烦。2.2 环境准备与版本选择这个项目的环境组合我建议采用一套比较稳重、兼容性好的版本别追求最新JDK 8目前绝大多数高校教学和企业的老项目都在用生态稳定。Maven 3.6.x用Maven来管理依赖一定要提前配好仓库镜像否则下载依赖会让人崩溃。Tomcat 8.5兼容Servlet 3.1规范和Spring 5.x配合很舒服。MySQL 5.7经典稳定版5.7以上有点新有些学校的机房可能不支持。IDEA毕业设计阶段直接用IDEA社区版就够用了别纠结Eclipse。关于JDK和Maven的环境变量配置我多说一句。很多同学卡在java不是内部或外部命令这一关其实就是JAVA_HOME没配好或者path里没加%JAVA_HOME%\bin。Maven除了MAVEN_HOME还要注意在path里加%MAVEN_HOME%\bin然后命令行敲mvn -v验证。IDEA里每次新建项目时要检查一下Project Structure里的JDK版本和Maven的settings路径这两处最容易出现代码能写但跑不起来的情况。2.3 前端方案JSP还是Vue很多学生纠结前端到底用不用Vue。我这个项目用的是JSP Bootstrap LayUI的组合理由很实在技术栈统一没有跨域问题不需要单独部署前端工程一个Tomcat全搞定。后端渲染JSP时可以直接使用EL表达式和JSTL标签页面展示状态、判断按钮是否显示都很方便。毕设的重点是后端流程和业务逻辑前端没必要给自己增加学习成本。如果你确实会用Vue想展示前后端分离的能力我也支持。但要注意前后端分离意味着要处理跨域、Token认证、接口文档这些额外的内容工作量至少增加30%。如果时间紧老老实实JSP就够了。3. 数据库设计状态机是整个项目的灵魂3.1 核心表结构设计这个系统的数据库我建议按7张核心表来设计别贪多也别太少表名用途说明sys_user用户表 包含用户名、密码、姓名、所属学院、角色ID、联系电话sys_role角色表 角色编码、角色名称project_application申报项目表 课题名称、项目类型、申报人、所属学院、经费预算、研究周期、状态、驳回意见approval_record审批记录表 项目ID、审批人、审批角色、审批结果、审批意见、审批时间project_attachment附件表 项目ID、原始文件名、存储文件名、上传人、上传时间expert_review专家评审表 项目ID、专家ID、评分、评审意见、评审时间、是否匿名notice_message通知消息表 接收人、关联项目ID、消息内容、是否已读、创建时间申报项目表是绝对的核心表字段上我建议重点关注这几个project_code是唯一的申报编号类似于申报[2025]001号通过程序生成project_type保存的是数据字典的编码不要直接存自然科学这种字符串application_status就是状态机字段下面重点讲。3.2 状态机设计0到6的流转规则状态机是清单里最值得讲的设计。申报项目的状态我设计成了这样一组数字0 草稿申报人创建但未提交可修改可删除。1 待学院初审申报人已提交学院科研秘书可见并可受理。2 学院初审通过科研处可见学院初审通过。3 科研处复审通过进入专家评审阶段。4 已立项评审通过科研处确认立项。5 学院驳回申报人可查看驳回理由并进行修改。6 科研处驳回同上。这套状态流转规则其实并不复杂就是一个有向的流程。但要注意驳回的状态不能合并成同一个数字因为申报人需要知道被谁驳回了不同角色驳回后的处理动作也不一样。数据库中我单独用一列application_status存这个数字。在页面展示时通过数据字典把状态数字翻译成中文标签。用户看到的是学院初审通过代码里是2这样安全且灵活。3.3 审批记录独立建表的原因审批记录单独建表是非常明确的设计决策我甚至建议你在论文里用一段话专门说明这个设计意图审批记录表存的是每一次操作的时间、操作人、角色、动作、意见。它的价值体现在三个场景一是答辩时评委问你怎么追溯一个项目的审批历史你直接演示时间线即可二是统计分析时需要统计每个审批环节的平均耗时没有这张表做不到三是审计需要高校的科研经费管理有合规性要求审批留痕是刚需。表结构我设计为CREATE TABLE approval_record ( id bigint(20) NOT NULL AUTO_INCREMENT, project_id bigint(20) NOT NULL COMMENT 申报项目ID, approver_id bigint(20) NOT NULL COMMENT 审批人ID, approver_role int(2) NOT NULL COMMENT 审批人角色, approve_action varchar(32) NOT NULL COMMENT 动作描述, approve_opinion varchar(500) DEFAULT NULL COMMENT 审批意见, create_time datetime NOT NULL COMMENT 操作时间, PRIMARY KEY (id), KEY idx_project_id (project_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT审批记录表;千万要注意approve_opinion要允许为空因为学院科研秘书受理时不一定要填写意见。如果设置成NOT NULL前端就必须强制填写反而把流程搞僵化了。字段的约束一定要跟着真实业务走不要为了省事全设成必填。4. 核心模块实现从登录到全流程落地4.1 登录与权限拦截器用户表的密码我采用的是MD5加盐方式存储。具体做法是password MD5(明文密码 用户名)为什么要把用户名作为盐原因是每个用户的盐不同即使两个用户密码相同加密结果也不同而且实现简单。权限控制我用SpringMVC拦截器来做配合角色编码判断。关键代码如下public class AuthInterceptor extends HandlerInterceptorAdapter { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); SysUser user (SysUser) session.getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return false; } String requestUri request.getRequestURI(); // 根据用户角色判断是否允许访问该URL if (!checkAuth(user.getRoleCode(), requestUri)) { response.setStatus(403); response.sendRedirect(request.getContextPath() /noPermission.jsp); return false; } return true; } }注意几个细节未登录和权限不足必须分开处理否则用户体验特别差静态资源要想清楚放行范围不然CSS和JS全部被拦掉拦截器的URL规则要覆盖所有需要权限的Controller路径建议在自定义的URLPermissionMap中集中管理不要散落在代码里。4.2 申报书CRUD与MyBatis动态SQL申报书列表是系统功能最重的一个模块难点在于多条件组合筛选。学生通常希望按项目名称模糊查询、按项目类型筛选、按学院筛选、按状态筛选这几个条件是组合的不是单选一。用MyBatis的动态SQL来解决就很适合select idselectByCondition resultTypemap parameterTypemap SELECT pa.*, su.real_name AS applicant_name, su.college_id, dict.label AS status_label FROM project_application pa LEFT JOIN sys_user su ON pa.applicant_id su.id LEFT JOIN sys_dict dict ON dict.type_code APPLICATION_STATUS AND dict.value pa.application_status where if testprojectName ! null and projectName ! AND pa.project_name LIKE CONCAT(%, #{projectName}, %) /if if testprojectType ! null and projectType ! AND pa.project_type #{projectType} /if if testcollegeId ! null and collegeId ! AND su.college_id #{collegeId} /if if teststatus ! null AND pa.application_status #{status} /if if testapplicantId ! null AND pa.applicant_id #{applicantId} /if /where ORDER BY pa.create_time DESC /select这段SQL有几个关键点状态字段要关联数据字典翻译成中文标签学院筛选字段用的是user表的college_id因为申报人是用户表里的申请人ID的筛选条件专门用于教师登录后只看到自己项目的场景这个条件不能在Java代码里硬拼因为MyBatis的#{}会帮我们处理SQL注入风险。分页我是用PageHelper插件实现的这个插件做SSM项目极其常用引入依赖后一行代码就搞定分页PageHelper.startPage(pageNum, pageSize); ListMapString, Object list declarationMapper.selectByCondition(params); PageInfoMapString, Object pageInfo new PageInfo(list);要注意PageHelper.startPage只能作用于紧接着执行的第一条SQL查询。如果前后有别的查询分页就会错位甚至报错。这个问题我踩过后面在坑的部分再细说。4.3 状态流转的实现状态流转是整个系统最核心的操作本质上就三件事更新申报表的状态字段、插入一条审批记录、生成一条消息通知给申请人。这三件事必须在同一个事务里完成否则状态变了审批记录没写就乱套了。代码大概是这样的Service Transactional public class ApprovalServiceImpl implements ApprovalService { Override public void firstInstituteApprove(Integer projectId, Integer approverId, boolean approve, String opinion) { ProjectApplication project projectApplicationMapper.selectById(projectId); if (project null) { throw new ServiceException(申报项目不存在); } // 判断当前状态必须是待学院初审 if (project.getApplicationStatus() ! 1) { throw new ServiceException(当前状态不允许此操作); } int newStatus approve ? 2 : 5; projectApplicationMapper.updateStatus(projectId, newStatus, approve ? null : opinion); ApprovalRecord record new ApprovalRecord(); record.setProjectId(projectId); record.setApproverId(approverId); record.setApproverRole(2); // 学院科研秘书 record.setApproveAction(approve ? 学院初审通过 : 学院初审驳回); record.setApproveOpinion(opinion); approvalRecordMapper.insert(record); noticeMessageMapper.insert(new NoticeMessage(project.getApplicantId(), projectId, 您的申报项目《 project.getProjectName() 》 (approve ? 已通过学院初审 : 被学院驳回请查看意见修改), false)); } }有一个细节值得单独拿出来讲状态流转的判断不能只靠前端隐藏按钮后端Service方法里必须再次校验当前状态。因为请求可以被构造直接发HTTP请求跳过一个状态是完全可能的。如果后端不校验状态就执行流转那整个流程就形同虚设了。4.4 文件上传模块高校科研申报中附件是刚需申报书PDF、查新报告、成果佐证材料都需要上传下载。文件上传我用的是CommonsMultipartResolver配置方法如下bean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver property namemaxUploadSize value52428800/ property namedefaultEncoding valueUTF-8/ /beanmaxUploadSize设置为50MB因为有些扫描版的查新报告会很大。前端同时要在form表单里设置enctypemultipart/form-data并且用input typefile namefile。文件存储我采用了本地目录存储 数据库存路径的方式。在服务器上建立一个upload目录文件名用UUID重命名防止撞名。数据库的project_attachment表保存原始文件名和存储的路径这样下载时把路径拿出来拼接完整路径即可。这里有个坑SpringMVC中处理文件上传的Controller参数类型必须是MultipartFile如果你用了File类型会一直报错。对应的依赖必须引入同时注意commons-io和commons-fileupload两个JAR包都要有只引入一个会导致找不到类。4.5 消息通知模块消息通知模块很多人会忽略但它对体验提升特别明显。教师登录后首页能看到我的待处理通知比如您的申报项目已通过学院初审。实现很简单在审批动作完成后向notice_message插入一条记录用户上线查询未读消息即可。查询时按创建时间倒序、只查未读的用了消息模块之后我的申报进度页面甚至可以直接做成通知列表展示不需要用户逐个点开项目看状态。5. 实操中踩过的坑能救一个是一个5.1 Maven依赖冲突SSM项目最常见的依赖坑是JSON库。我一开始项目里同时引入了fastjson和jackson结果SpringMVC在做ResponseBody返回JSON时莫名其妙地报错有的方法能转有的方法转换出来是一堆乱码。原因就是两个库的类都在classpath里Spring在适配时出现了冲突。解决的办法是删掉一个统一用fastjson因为老项目很多资料和Demo都是基于fastjson的。另外插件版本的对应关系也容易出错IDEA的Maven插件太新、项目用的Maven版本太老会出现Failed to execute goal这类问题。我的建议是Maven版本不要超过3.83.6.3是一个很稳的选择。5.2 Mapper接口与XML映射文件的“隐形”错误有一个错误极其隐蔽UserMapper接口的namespace和XML文件里的namespace不一致或者方法名对不上或者resultType写错。这种错误在编译期完全发现不了只有到运行期执行SQL时才报BindingException: Invalid bound statement (not found)。排查方法我总结成三个查一查namespace是否等于接口全限定名二查select标签的id是否等于接口方法名三查resultType或resultMap的类型是否存在且字段名匹配。还有一个容易忽略的地方MyBatis在Spring配置里的mapper-locations路径如果没有匹配到XML文件比如路径写成了classpath:/mapper/*.xml但XML实际在别的包下那同样会报上面的异常。5.3 事务死活不生效我要讲的这个坑特别经典。我测试驳回操作时发现状态字段没有更新但审批记录却插入了数据完全不一致。排查了好久才找到原因Service方法内部同一个类中一个方法调用了另一个Transactional方法事务没有加在代理对象上导致事务没有包裹住嵌套调用的方法。解决办法是把事务注解加在对外暴露的公开方法上保证通过Spring代理调用。如果你一定要在类内调用带事务的方法可以往要调用的Service里注入自己或者把被调方法拆到另一个Service中。关于这一点面试官也特别喜欢问答好了很加分。5.4 中文乱码中文乱码在SSM时代属于必现问题基本和每个做SSM的人都会碰面。典型场景页面显示中文正常但提交到后台后变成问号或者JSP页面往浏览器输出时乱码。根本原因在于编码链路上某一个环节不一致。我的做法是设置一条龙JSP页面统一pageEncodingUTF-8SpringMVC配置里加CharacterEncodingFilter过滤器强制使用UTF-8并设forceEncodingtrueTomcat的server.xml中Connector配置URIEncodingUTF-8数据库连接串加上characterEncodingutf8。这套配置下来乱码问题基本绝迹。5.5 MySQL连接参数MySQL 5.7在项目启动时报SSL警告或Public Key Retrieval is not allowed错是特别普遍的现象。解决办法是在JDBC连接串上加参数jdbc:mysql://localhost:3306/scientific_research?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueserverTimezone尤其关键不设置时区在插入时间时会报错或者时间少8小时。很多同学被卡了一下午问题就出在这个参数上。5.6 分页PageHelper和表连接的分页混乱使用List集合时PageHelper没有出问题。可一旦有多表连接查询和分组统计容易出分页总数不对的问题。原因是PageHelper拦截的只是被标记的SQL如果业务代码中先执行了另一条无关SQLPageHelper就会把分页加错地方。建议把分页查询独立成单一方法不要和统计查询混在一起。还有一个更简单的替代方案不用PageHelper直接手工用LIMIT #{offset}, #{pageSize}拼分页参数MyBatis传参也不难遇到复杂SQL时可控性更好。6. 答辩准备与项目扩展思路6.1 答辩必问知识点清单做完这个项目最关键的一步是把SSM相关的基础知识背扎实。考察重点集中在几个问题上Spring IoC到底是什么对象创建和依赖注入交给容器管理我们只声明类之间的依赖关系。举申报Service注入申报Mapper这个例子一句话就能讲清。Spring AOP用在什么地方项目中事务管理就是典型的AOP应用通过声明式事务注解把事务逻辑从业务代码中剥离出来。SpringMVC的执行流程请求从DispatcherServlet开始经过HandlerMapping找处理器HandlerAdapter调用Controller返回ModelAndViewViewResolver解析视图渲染后返回。MyBatis的#{}和${}有什么区别#{}是预编译参数占位符能防止SQL注入${}是字符串拼接有注入风险。筛选条件和排序字段要用#{}动态排序时如果有特殊需求要用${}但必须做好白名单校验。这些知识点如果你能结合自己项目里的具体场景来讲评委就不会觉得你在背八股文。比如讲MyBatis动态SQL时直接打开你的XML文件指着if标签说你如何实现组合筛选又直观又可信。6.2 代码层面的加分项在答辩演示环节有几个不起眼但很细节的加分点值得提前做所有删除操作必须加确认避免误删除。Controller接收参数时使用实体对象和RequestParam不要用HttpServletRequest获取参数。加一个全局异常处理器页面友好提示系统繁忙而不是一堆堆栈信息。给核心操作加上操作日志比如登录日志、审批日志这个用Spring AOP的环绕通知很容易实现演示效果极具说服力。6.3 后续扩展思路答辩时评委常问你这个系统还能怎么改进。你可以准备三个递进式的思路一是使用Spring Boot重构整个工程减少格式配置量二是引入Redis缓存热点数据比如数据字典、项目状态统计结果三是改造成前后端分离架构用VueElementUI做前端页面接口对接采用Restful风格。但注意说扩展不等于做扩展毕设的核心仍是完成度和稳定运行不要为了堆技术把范围撑到无法收尾。我的切身体会是高校科研申报系统这类管理项目真正拉开差距的地方不在代码量而在三件事能不能把状态机的流转讲清楚能不能把数据隔离和权限设计交代明白能不能把你的真实开发过程讲给人听。你稍微多花一点时间把流程中的细节做通、把异常路径想全答辩结果通常都不会差。如果你现在已经拿到这个题目先打开数据库工具把状态流转的图理顺再动手写代码你后面会感谢自己的。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进