每年结题季总能看同一场戏:项目负责人拿着十几个文件夹跑上跑下,验收专家对着纸质材料一张张翻,科研管理员的桌上堆着等待盖章的审批表。项目材料散在邮箱、微信、U盘里,数据统计要手动从Excel里抠出来拼。这套基于SpringBoot+Vue的科研项目验收管理系统,就是冲着这个痛点去的——把项目从申报、提交验收材料、专家分配、在线评审打分到结论归档的整条链路搬上线,后端用Java+SpringBoot+MyBatis操作MySQL,前端用Vue搭页面,整套源码可以完整跑通。
做这个项目的有三类人最合适:一是拿它当毕业设计或课程项目的学生,技术栈主流、业务完整度够,写论文有素材;二是小团队或课题组想搭一套内部项目管理系统,改改用得上;三是想系统过一遍前后端分离开发流程的Java学习者。下面从业务设计、技术选型、数据库、前后端实现到部署踩坑,按我做这个项目时的真实推进顺序展开。
1. 先把业务想清楚:科研项目验收到底要管哪些事
动手写代码之前,搞懂业务比选框架重要得多。我一开始也踩过这个坑,上来就建表写接口,写到一半发现验收流程里还有"专家回避""复议""延期申请"这些环节没地方放,返工成本很高。所以先花半天把业务流程捋清楚,后面基本是一路畅通。
1.1 谁在用这个系统:角色的职责划分
科研项目验收管理系统里,用户角色基本分成四类,每一类的使用场景和权限边界差别很大:
- 项目负责人:创建项目、填写基本信息、上传验收材料、查看验收进度和结论。
- 验收专家:查看被分配的项目材料,在线填写评分意见,提交验收结论。
- 科研管理员:审核项目信息、分配专家、管理验收批次、汇总结果、导出归档数据。
- 系统管理员:维护用户账号、配置角色权限、处理基础字典数据(项目类型、学科方向等)。
这里有一个设计上的关键点:专家不能自己选项目,管理员分配时还要考虑回避关系。如果项目负责人和专家属于同一个课题组,系统最好能自动提示,否则验收结果没有公信力。代码里实现一个简单的校验逻辑就行,但业务上一定要留这个字段。
1.2 验收流程的完整闭环:从提交材料到归档
一个标准的验收流程大概是这样跑的:
- 项目负责人填写项目申请信息,提交验收申请。
- 管理员审核材料完整性和格式,不通过则退回修改。
- 管理员根据项目的学科方向匹配专家库,分配2-3位评审专家。
- 专家登录系统,查看材料,逐项打分并填写验收意见。
- 系统根据评分规则汇总结果,生成验收结论(通过/不通过/整改后通过)。
- 管理员做最终确认,归档项目,导出验收记录。
流程看着不复杂,但状态会来回跳。比如管理员退回申请后项目回到"待修改",专家打分意见不一致时要进入"补充评审",归档之后又可能被"复议"捞起来。所以我建议在项目表里用一个状态字段贯穿始终,后端统一维护状态流转逻辑,而不是让前端各页面各改各的。
1.3 功能清单:哪些功能是刚需,哪些是锦上添花
我最终落地的功能清单如下,可以直接对标你自己要做的模块:
| 模块 | 功能 | 优先级 |
|---|---|---|
| 项目申报 | 项目建档、修改、删除、提交申请 | 必须 |
| 材料管理 | 验收材料上传、下载、预览 | 必须 |
| 专家管理 | 专家库维护、专家分配、回避校验 | 必须 |
| 在线评审 | 打分表、意见填写、提交结论 | 必须 |
| 结果汇总 | 自动计算均分、生成结论、导出 | 必须 |
| 系统管理 | 用户、角色、字典、日志管理 | 建议 |
| 数据看板 | 按学院/年度统计验收通过率 | 可选 |
做管理系统最忌讳的就是"什么都想要",比如数据分析、消息推送、流程引擎都往里塞。实际开发下来,消息推送可以用简单的站内信实现,流程引擎用状态字段加Service层判断就够,过度设计会让源码的可读性很差,答辩时也不好讲。
2. 技术选型复盘:为什么是SpringBoot+Vue+MyBatis这一套
这个项目标题的技术栈是很多人熟悉的配方: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的HQL,MyBatis的优势就在于"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) { List<ReviewScore> 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:列宽度不要全交给组件自适应,关键列设固定宽度,操作列设fixed="right",否则表格在宽屏和窄屏下的表现差很多。el-form:rules校验规则要写在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-case=true,让数据库的下划线字段自动转成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里加上serverTimezone=Asia/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里显式指定serverTimezone=Asia/Shanghai。
还有一个前端显示相关的问题:后端返回的时间是2024-06-30T10:00:00这种带T的格式,前端ElementUI的日期组件不认。我做了统一的处理:后端用@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")把所有时间字段格式化成可读格式,前端就不需要额外解析了。
7.4 前后端联调时的时间字段格式
这个坑我排了很久。专家评审页面提交评分数据后,列表页显示评审时间一直是null,因为前端传给后端的时间字段格式是ISO格式字符串,Jackson默认反序列化失败,字段赋值失败后数据直接丢成了null。
最后的解决办法是在全局Jackson配置里注册一个ObjectMapper,自定义日期格式yyyy-MM-dd HH:mm:ss,并把FAIL_ON_UNKNOWN_PROPERTIES关掉。关掉这个开关后,前端多传的字段不会导致接口报错,对联调阶段的容错很有帮助。这也是一个小经验:前后端联调时,后端不要对未知字段太严格,宁可忽略边缘数据,也不要让整个请求崩掉。
再说一条项目开发中比较反常识的经验:这类管理系统,数据库的初始化数据比代码本身更重要。没有测试数据,你连登录都登不进去,更别说演示验收流程。我在初始化脚本里预置了角色、用户、专家、项目各若干条,保证任何一步操作都有数据可走。你写完这个项目准备答辩或者演示的时候,会发现这套预置数据让你省了太多临场造数据的尴尬。
整个项目做完回头看,最有价值的不是某一个炫技的功能,而是把"项目申报——专家评审——结果归档"这条链路完整地、稳定地跑通了。写代码的人容易低估业务梳理和数据库设计的分量,总觉得后端接口难写,其实真正决定一个管理系统能不能用的,是状态流转是否严谨、提示信息是否到位、数据统计是否准确。这套代码你拿到手跑通之后,建议自己动手改两个地方——比如把评审维度变成可配置的字典表,或者给消息通知加上站内信模块——你会发现改完这两个需求,你对前后端分离开发的理解会上一个台阶。