每年结题季总能看同一场戏项目负责人拿着十几个文件夹跑上跑下验收专家对着纸质材料一张张翻科研管理员的桌上堆着等待盖章的审批表。项目材料散在邮箱、微信、U盘里数据统计要手动从Excel里抠出来拼。这套基于SpringBootVue的科研项目验收管理系统就是冲着这个痛点去的——把项目从申报、提交验收材料、专家分配、在线评审打分到结论归档的整条链路搬上线后端用JavaSpringBootMyBatis操作MySQL前端用Vue搭页面整套源码可以完整跑通。做这个项目的有三类人最合适一是拿它当毕业设计或课程项目的学生技术栈主流、业务完整度够写论文有素材二是小团队或课题组想搭一套内部项目管理系统改改用得上三是想系统过一遍前后端分离开发流程的Java学习者。下面从业务设计、技术选型、数据库、前后端实现到部署踩坑按我做这个项目时的真实推进顺序展开。1. 先把业务想清楚科研项目验收到底要管哪些事动手写代码之前搞懂业务比选框架重要得多。我一开始也踩过这个坑上来就建表写接口写到一半发现验收流程里还有专家回避复议延期申请这些环节没地方放返工成本很高。所以先花半天把业务流程捋清楚后面基本是一路畅通。1.1 谁在用这个系统角色的职责划分科研项目验收管理系统里用户角色基本分成四类每一类的使用场景和权限边界差别很大项目负责人创建项目、填写基本信息、上传验收材料、查看验收进度和结论。验收专家查看被分配的项目材料在线填写评分意见提交验收结论。科研管理员审核项目信息、分配专家、管理验收批次、汇总结果、导出归档数据。系统管理员维护用户账号、配置角色权限、处理基础字典数据项目类型、学科方向等。这里有一个设计上的关键点专家不能自己选项目管理员分配时还要考虑回避关系。如果项目负责人和专家属于同一个课题组系统最好能自动提示否则验收结果没有公信力。代码里实现一个简单的校验逻辑就行但业务上一定要留这个字段。1.2 验收流程的完整闭环从提交材料到归档一个标准的验收流程大概是这样跑的项目负责人填写项目申请信息提交验收申请。管理员审核材料完整性和格式不通过则退回修改。管理员根据项目的学科方向匹配专家库分配2-3位评审专家。专家登录系统查看材料逐项打分并填写验收意见。系统根据评分规则汇总结果生成验收结论通过/不通过/整改后通过。管理员做最终确认归档项目导出验收记录。流程看着不复杂但状态会来回跳。比如管理员退回申请后项目回到待修改专家打分意见不一致时要进入补充评审归档之后又可能被复议捞起来。所以我建议在项目表里用一个状态字段贯穿始终后端统一维护状态流转逻辑而不是让前端各页面各改各的。1.3 功能清单哪些功能是刚需哪些是锦上添花我最终落地的功能清单如下可以直接对标你自己要做的模块模块功能优先级项目申报项目建档、修改、删除、提交申请必须材料管理验收材料上传、下载、预览必须专家管理专家库维护、专家分配、回避校验必须在线评审打分表、意见填写、提交结论必须结果汇总自动计算均分、生成结论、导出必须系统管理用户、角色、字典、日志管理建议数据看板按学院/年度统计验收通过率可选做管理系统最忌讳的就是什么都想要比如数据分析、消息推送、流程引擎都往里塞。实际开发下来消息推送可以用简单的站内信实现流程引擎用状态字段加Service层判断就够过度设计会让源码的可读性很差答辩时也不好讲。2. 技术选型复盘为什么是SpringBootVueMyBatis这一套这个项目标题的技术栈是很多人熟悉的配方SpringBoot Vue MyBatis MySQL。它不新潮但胜在稳妥。我复盘一下每个环节的取舍逻辑顺便说说为什么没有换掉其中某一环。2.1 SpringBoot解决的是开发效率问题早期做JavaWeb项目要自己配Spring、SpringMVC、一堆XML光交给容器的配置就够折腾半天。SpringBoot把自动配置 约定大于配置做到位内置Tomcat一个Application类启动整个后端写接口基本就是Controller - Service - Mapper三层往下递。对于这种中小型管理系统开发速度是第一位的SpringBoot把框架的复杂度消化在引擎内部开发者可以专心写业务。2.2 Vue2还是Vue3怎么定项目标题只写了Vue没说版本。我的建议是如果你是跟着毕设题目走题目里没有特别指定就看你手头的参考资料。Vue2的生态资料更全ElementUI组件库跟Vue2的配合更成熟很多网上找的教学案例都是Vue2写法照葫芦画瓢快Vue3 Element Plus的组合更适合从零开始学新项目的人Composition API的代码组织方式写大一点的管理系统其实更舒服。我这个项目最终用的是Vue2 ElementUI Axios Vue Router Vuex因为这套资料多、插件全、踩坑文档丰富目标是把功能跑通而不是追新。2.3 MyBatis在半动态SQL场景下的优势管理系统的查询条件特别多项目名称模糊查询、项目类型下拉筛选、状态多选、时间范围过滤。这些条件组合起来SQL里的WHERE子句要动态拼接。MyBatis的if、where、foreach标签处理这种场景非常顺手写动态SQL就像搭积木。对比JPA的Specification和Hibernate的HQLMyBatis的优势就在于SQL是你的又不是你的——你完全掌控SQL写法能做细粒度的性能优化。2.4 为什么没有选更重的方案也有人问过为什么不加个Redis缓存、用SpringCloud微服务拆分、或者上ShardingSphere分库分表答案很简单系统规模匹配技术复杂度。一套面向单一单位内部的验收管理系统并发量撑死几十个人同时用单机MySQL配合索引优化完全够。引入微服务等于给自己增加服务注册、网关、链路追踪一整套额外负担对毕设或者小团队内部工具来讲是负收益。3. 数据库设计验收项目的表结构该怎么拆表结构设计是这类管理系统能不能让人顺手用起来的分水岭。我设计的原则是把人和业务分开把主表和过程数据分开。项目基本信息是一张主表验收过程中的专家分配、评审打分、材料记录都作为关联数据存成独立表这样查询和统计都灵活。3.1 核心表关系梳理我的数据库里核心表大概有这么几张project项目主表承载基本信息、负责人员、当前状态。project_material验收材料表项目关联的附件文件记录。expert专家信息表维护专家的学科方向和联系方式。expert_assignment专家分配记录表记录哪个专家被分到哪个项目。review_score评审打分表一份项目对应多位专家的多维度得分。review_opinion评审意见表存专家填写的文字意见和结论建议。user系统用户表。role、user_role角色及用户角色关联表。外键关系不要滥用。我在表结构里建立了逻辑关联但不一定都加物理外键因为MyBatis做多表查询时主要靠JOIN物理外键反而在批量删除和迁移数据时容易触发约束问题。3.2 项目主表与材料表的设计要点项目主表的字段设计重点在状态的粒度和统计维度。我的project表里关键字段是project_name项目名称project_code项目编号唯一索引leader_name、leader_phone负责人信息project_type项目类型比如重点/面上/青年subject_field学科方向用于专家匹配status当前状态提交/审核中/待评审/评审中/已完成apply_time、archive_time申请与归档时间这里有一个容易被忽略的细节project_code一定要加唯一索引因为一个单位里可能有重名项目但编号必须唯一。材料表project_material里需要存文件在服务器上的相对路径而不是直接存文件二进制。数据库存二进制的做法会让表体量迅速膨胀备份困难上传下载也不方便。我用的方案是文件落盘到指定目录数据库里存文件名、存储路径、上传时间、文件大小、上传人ID。3.3 验收评审表打分规则如何落库评审打分不能只存一个总分因为验收通常有多个维度比如任务完成情况成果指标达成度经费使用规范性。我的review_score表设计成了维度明细project_id关联项目expert_id关联专家dimension_name维度名称dimension_score该维度得分total_score专家给的总分review_date评审日期这样设计的好处是前端拿到数据后可以直接用GROUP BY project_id算平均分也能支持分维度横向对比导出汇总表时不用做二次解析。给验收结论定规则的时候我在Service层里写死了一个默认逻辑平均分≥80分且所有专家结论均为通过时项目自动判定为通过若平均分在60-80分之间判定为整改后通过平均分低于60分判定为不通过。这个规则可以在系统管理里做成配置项但默认写死在代码里更直观答辩时也好解释。3.4 状态字段的重要性与设计陷阱管理系统里最容易出bug的地方就是状态管理。我见过有人把项目状态在前端页面里用localStorage存页面一刷新状态就丢了数据不一致。正确做法是状态永远以数据库字段为准前端展示和操作按钮都向后端接口要实时数据。另外有个小陷阱状态字段用数字0-5还是字符串PENDING、REVIEWING中文还是英文从可读性角度我建议在数据库里用字符串存英文枚举值比如DRAFT、SUBMITTED、UNDER_REVIEW、COMPLETED在Java里对应枚举类前端再映射成中文标签。这样数据库里看到的数据是自解释的不用翻字典表。4. 后端实现权限控制与验收流程的状态机后端是整个系统的核心实现方案决定数据可信度。这一节重点讲登录认证、角色权限、状态流转和文件上传这几个模块的实现思路和关键代码逻辑。4.1 登录认证与角色权限的落地方式这个项目没有上Spring Security这套重型框架因为验收系统的操作路径不算多用拦截器加自定义注解就能实现权限切面。登录成功后生成一个Token存到Redis或者直接放在内存Map里后续每次请求在请求头带上Token拦截器验证Token后从数据库查出用户角色再判断接口权限。我用的是自定义注解 SpringMVC拦截器的方式定义一个RequireRole注解注解里标value为admin或expert或leader。拦截器里读取请求对应处理方法的注解判断当前用户角色是否匹配。不匹配就直接返回403统一错误码前端根据错误码做路由跳转。这样做清爽可控。如果上Spring Security虽然功能全但配置项多、概念多写毕设想快速跑通功能的话性价比不高。等以后系统需要接入OAuth2、第三方登录时再迁移也不迟。4.2 验收流程的状态流转设计状态流转是这类系统最核心的逻辑我把它抽象成一个控制器方法专门处理PostMapping(/project/transition) RequireRole(admin) public Result transitionProject(RequestBody TransitionRequest request) { // 校验当前状态是否允许目标状态不允许则抛出业务异常 if (!statusValidator.canTransit(project.getStatus(), request.getTargetStatus())) { return Result.error(当前状态不允许执行该操作); } project.setStatus(request.getTargetStatus()); projectMapper.update(project); // 记录状态变迁日志后续可以查历史轨迹 statusLogMapper.insert(new StatusLog(project.getId(), project.getStatus(), request.getTargetStatus())); return Result.success(); }状态间允许的合法路径我单独写了一个StatusValidator类维护。比如DRAFT可以到SUBMITTED但不可以直接到COMPLETED。SUBMITTED可以到UNDER_REVIEW也可以由管理员退回为DRAFT。UNDER_REVIEW只能在所有专家都提交评审结果后才能流转到PENDING_ARCHIVE。COMPLETED是终态只有管理员可以归档。有人问为什么要单独维护合法路径而不是每个状态写一个方法因为状态多了以后每个方法开头都有一堆if判断写起来重复还容易漏掉边界场景。集中到一个校验器里想看全流程规划打开一个类就能读清楚。4.3 文件上传与下载的实现细节文件上传是验收系统的刚需——项目负责人要传结题报告、论文列表、成果证明专家要下载材料。这里有几个细节得处理到位配置文件上传大小限制spring.servlet.multipart.max-file-size和max-request-size默认1MB是不够用的我设置成了50MB。文件名重命名避免中文名导致跨平台乱码也避免特殊字符带来的安全问题。我用UUID生成新文件名原文件名存入数据库。存储目录按项目ID分文件夹/data/uploads/{projectId}/方便清理和按项目汇总。下载时设置响应头Content-Disposition要处理成attachment文件名用URLEncoder编码。PostMapping(/material/upload) public Result upload(RequestParam(file) MultipartFile file, RequestParam(projectId) Long projectId) { if (file.isEmpty()) { return Result.error(上传文件不能为空); } String originalFilename file.getOriginalFilename(); String extName originalFilename.substring(originalFilename.lastIndexOf(.)); String storedName UUID.randomUUID().toString().replace(-, ) extName; String dirPath uploadPath File.separator projectId; File dir new File(dirPath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, storedName)); materialMapper.insert(new ProjectMaterial(projectId, originalFilename, storedName, file.getSize(), currentUserId())); return Result.success(); }这一段代码量不大但覆盖了校验、存储、落库三个环节属于一套标准做法。实际部署时建议把上传目录配成绝对路径别放在项目打包路径下否则每次重新部署服务文件就丢了。4.4 打分与结果汇总的后端逻辑专家打分接口的核心不是insert而是防重和锁。一个专家对一个项目的打分只能有一条记录第二次提交就是更新不能insert出两条否则汇总平均分就乱套了。我在Mapper里用一个联合唯一索引(project_id, expert_id)来兜底代码层面再做一步先查后插。结果汇总是这样计算的public ReviewSummary summarize(Long projectId) { ListReviewScore scores reviewScoreMapper.selectByProjectId(projectId); if (scores.isEmpty()) { return null; } double avg scores.stream().mapToDouble(ReviewScore::getTotalScore).average().orElse(0); boolean allPass scores.stream().allMatch(s - PASS.equals(s.getConclusion())); String finalConclusion; if (avg 80 allPass) { finalConclusion PASS; } else if (avg 60) { finalConclusion NEED_RECTIFY; } else { finalConclusion FAIL; } return new ReviewSummary(avg, finalConclusion); }这段逻辑要注意一个点如果项目被分配了3位专家但只有2位提交了评分这时候不能急着汇总。我是在UNDER_REVIEW流转PENDING_ARCHIVE之前查出已提交专家数和分配专家数做一次数量比对完全匹配才允许流转。5. 前端实现Vue项目里的页面组织与接口对接前端部分这个项目用的是Vue2 ElementUI Axios整体是一个典型的单页后台管理系统结构。我按目录职责、页面拆解、接口封装三层来讲。5.1 前端工程结构怎么组织我的src目录结构没有那么花哨胜在直观src/ api/ // 每个模块的接口请求定义 assets/ // 静态资源 components/ // 公共组件 router/ // 路由配置 store/ // Vuex状态管理 views/ // 页面组件 project/ // 项目管理页 expert/ // 专家管理页 review/ // 评审页面 dashboard/ // 数据看板 utils/ // 请求封装、工具函数前后端分离项目的最大误区就是前端把页面的数据临时状态存得太散。我统一用一个request.js封装Axios实例设置baseURL和请求拦截器把Token自动加到请求头里响应拦截器统一处理401跳登录和业务错误码提示。这样每个API模块只需要写请求方法和参数重复工作少很多。5.2 核心页面拆解项目管理页、评审页项目管理页是我花时间最多的页面。它有两个表格项目列表表格和状态时间线。列表表格的查询区有项目名称、类型、状态几个筛选条件后端接口用MyBatis动态SQL去拼查询条件。操作栏的按钮需要根据状态动态渲染比如DRAFT状态下显示提交和编辑SUBMITTED状态下显示撤回UNDER_REVIEW状态下显示分配专家。这种东西写在前端模板里判断就行但状态判断的逻辑一定要跟后端字段对齐前端写死中文判断等于埋雷。评审页的核心是一个评分表单。项目负责人上传完材料专家点开项目详情左侧是材料预览列表可以逐个下载查看右侧是评审表单。评审表单用了ElementUI的el-form和el-input-number组件每个维度一个数字输入框范围是0-100提交前做校验所有维度必须填完总分等于各维度之和这个校验规则是我自己定的如果你的项目维度是加权平均可以各自独立。5.3 表格、表单、文件上传这三个组件的使用节奏ElementUI在这三个场景里的体验做得很好但也有几个容易踩的位置el-table列宽度不要全交给组件自适应关键列设固定宽度操作列设fixedright否则表格在宽屏和窄屏下的表现差很多。el-formrules校验规则要写在data里而不是模板里写一堆prop否则代码不可读。文件上传我用的是el-upload的http-request自定义方式手动把文件传给后端的/material/upload接口而不是用它的默认action配置这样Token能从我们自己的Axios实例带过去绕开默认的跨域和认证问题。handleUpload({ file }) { const formData new FormData(); formData.append(file, file); formData.append(projectId, this.currentProjectId); uploadMaterial(formData).then(res { this.$message.success(上传成功); this.getMaterialList(); }); }5.4 接口对接时的数据格式对齐前后端联调时最烦的就是数据字段名对不上。比如后端返回projectName前端写成了project_name页面直接空白。我的做法是后端统一使用驼峰命名配置MyBatis的map-underscore-to-camel-casetrue让数据库的下划线字段自动转成Java的驼峰字段Jackson序列化后给前端的就是projectName这种格式。前端API模块里每个函数都写清楚请求参数和返回结构// api/project.js export function getProjectList(params) { return request({ url: /api/project/list, method: get, params }); } export function uploadMaterial(data) { return request({ url: /api/material/upload, method: post, data }); }想做好系统性前后端的数据结构定义最好写在同一个文档里哪怕就是一份简单的txt都对联调有益。6. 源码跑通与部署复现从0到能点出全流程这个项目被称为带完整源码所以我把从环境准备到部署的完整路径写一遍照着走基本都能跑起来。6.1 环境准备和初始化清单需要准备的东西JDK 1.8或以上推荐JDK8。Maven 3.6用来构建打包后端。MySQL 5.7或8.0注意8.0的驱动名和时区配置略有差异。Node.js 14用来跑前端构建。IDEA或VSCode任选。初始化流程创建一个空的MySQL数据库字符集选utf8mb4。执行项目里的sql/init.sql脚本自动建表并插入初始化数据包含管理员账号和几个测试专家、项目。修改后端application.yml里的数据库连接、账号密码和上传目录路径。启动后端服务默认端口8080。进入前端目录执行npm install安装依赖再执行npm run dev默认端口9528。前端服务起来后浏览器访问http://localhost:9528用初始化脚本里的管理员账号登录就能看到项目列表页。6.2 后端启动步骤和常见报错后端启动其实很简单IDEA里打开项目等待Maven自动下载依赖然后运行主类里的main方法。如果启动失败八成是这么几个原因MySQL没启动或者数据库连接配置不对。报错信息一般是Communications link failure。端口被占用8080被别的程序占了。解决办法是改server.port或者杀掉占用进程。驱动和数据库版本不匹配。MySQL 8.0要用com.mysql.cj.jdbc.Driver并且连接URL里加上serverTimezoneAsia/Shanghai。我实际跑的时候遇到过一个问题Maven下载依赖特别慢挂了代理也没用后来换了阿里云镜像仓库地址速度肉眼可见地提升。这个配置改到Maven的settings.xml里一次配置全局生效。6.3 前端启动与代理配置前端npm install装完依赖后直接npm run dev。启动后页面能打开但点击登录时提示请求失败这种情况基本是接口没通。前后端分离的开发模式下前端页面在9528端口后端接口在8080端口浏览器直接跨域了。我的解决方案是在Vue开发服务器里配置代理把/api开头的请求转发到http://localhost:8080// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } };后端接口实际路径可能没有/api前缀所以要做一次pathRewrite。前端请求统一带上/api前缀后端不必感知这个前缀的存在。6.4 打包部署到服务器开发模式下跑通之后如果要部署到一台服务器流程是后端在项目根目录执行mvn clean package -DskipTests生成target/xxx.jar。前端执行npm run build生成的dist目录就是纯静态文件。把xxx.jar传到服务器用java -jar xxx.jar启动后端。把dist目录配置到Nginx的静态站点里并配置反向代理当访问的路径以/api开头时代理到本地8080端口。这里有一个容易忽略的点前端打包后的接口地址是写死的/api如果你的反向代理路径没配好页面能打开但请求全部404。Nginx配置里核心这一段location /api/ { proxy_pass http://127.0.0.1:8080/; }注意proxy_pass末尾的斜杠它会把/api前缀移除掉跟开发环境里的pathRewrite作用一致。如果忘了这个斜杠后端收到的URL会多一层/api路由对不上排查半天还以为是接口写错了。7. 项目开发中踩过的坑和规避建议最后这部分是纯实战总结。我做这个项目的过程中踩了几个代表性的坑整理出来希望对你有用。7.1 文件上传的几个典型问题第一个坑是上传文件大小超限。后端默认1MB限制传一个结题报告的PDF就报错报错信息被前端拦截器当成普通错误弹出来也没有明确提示。后来我配置了max-file-size为50MB并且在全局异常处理器里单独捕获MaxUploadSizeExceededException返回特定的错误码前端再针对这个错误码给出友好提示。第二个坑是文件落盘路径找不到。用IDE启动的时候相对路径指向的是模块目录部署成jar后路径又会变化。后来我统一改成配置项upload.path在application.yml里指定一个绝对路径如D:/data/uploads并在启动时判断目录是否存在、自动创建。第三个坑是文件下载时文件名中文乱码。Content-Disposition里的filename默认是ISO-8859-1编码中文名到浏览器就变成了问号。解决办法是转成UTF-8再URL编码前端收到响应头后自己解码展示。7.2 跨域问题的排查链路开发环境里前端请求报跨域错误我一开始在后端加CrossOrigin注解结果只对单个Controller生效另一个接口还是报错。后来统一改成配置类实现WebMvcConfigurer全局添加跨域映射。不过开发环境里最好的方案还是用前面提到的Vue代理因为代理方式根本不涉及跨域。跨域问题的排查链路我总结成三步先确认请求有没有发出去再看响应有没有被拦截最后观察浏览器控制台的报错类型。如果是CORS policy报错要么后端允许跨域要么前端用代理绕开如果是网络层直接失败那是代理配置或端口问题跟跨域无关。分清这两类排查效率高很多。7.3 数据库连接和时区问题这个坑看起来小但几乎每个人都会遇到。MySQL 8.0连接时区默认是UTC而我们的服务器是东八区时间字段存进数据库后取出来发现差了8个小时。解决方式是在连接URL里显式指定serverTimezoneAsia/Shanghai。还有一个前端显示相关的问题后端返回的时间是2024-06-30T10:00:00这种带T的格式前端ElementUI的日期组件不认。我做了统一的处理后端用JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)把所有时间字段格式化成可读格式前端就不需要额外解析了。7.4 前后端联调时的时间字段格式这个坑我排了很久。专家评审页面提交评分数据后列表页显示评审时间一直是null因为前端传给后端的时间字段格式是ISO格式字符串Jackson默认反序列化失败字段赋值失败后数据直接丢成了null。最后的解决办法是在全局Jackson配置里注册一个ObjectMapper自定义日期格式yyyy-MM-dd HH:mm:ss并把FAIL_ON_UNKNOWN_PROPERTIES关掉。关掉这个开关后前端多传的字段不会导致接口报错对联调阶段的容错很有帮助。这也是一个小经验前后端联调时后端不要对未知字段太严格宁可忽略边缘数据也不要让整个请求崩掉。再说一条项目开发中比较反常识的经验这类管理系统数据库的初始化数据比代码本身更重要。没有测试数据你连登录都登不进去更别说演示验收流程。我在初始化脚本里预置了角色、用户、专家、项目各若干条保证任何一步操作都有数据可走。你写完这个项目准备答辩或者演示的时候会发现这套预置数据让你省了太多临场造数据的尴尬。整个项目做完回头看最有价值的不是某一个炫技的功能而是把项目申报——专家评审——结果归档这条链路完整地、稳定地跑通了。写代码的人容易低估业务梳理和数据库设计的分量总觉得后端接口难写其实真正决定一个管理系统能不能用的是状态流转是否严谨、提示信息是否到位、数据统计是否准确。这套代码你拿到手跑通之后建议自己动手改两个地方——比如把评审维度变成可配置的字典表或者给消息通知加上站内信模块——你会发现改完这两个需求你对前后端分离开发的理解会上一个台阶。
阅读完成 · 觉得有帮助?