☰
SpringBoot+Vue+MySQL科研项目验收管理系统:从选题到答辩完整实战
2026/10/19 4:24:49 网站建设 项目流程

毕业设计选题的时候,我见过太多人掉进两个极端:一个是题目太“大而新”,比如搞一套分布式微服务的什么平台,三个月搭不起来,答辩时候四处漏风;另一个是题目太“小而旧”,就是普通增删改查,演示十秒钟就完了,老师问两句就没货了。最后我选了这个“科研项目验收管理系统”,技术栈限定在 SpringBoot + Vue + MySQL,业务上又带一条真实的验收流程,既不会失控,又能把话说透。

科研项目验收这个东西,平时听着离学生很远,实际上一所高校或科研机构每年在研项目几百个,结题时全都要走“提交材料 → 形式审核 → 专家评审 → 结论登记”这条路。没系统之前,科研处靠邮箱收材料、用共享表格分配专家、拿计算器汇总分数,一件不大的事被邮件反复拉扯放大,效率极低。这个项目要做的,就是把这套线下流程搬到线上,让项目负责人、科研处管理员、评审专家各就各位,把“项目、材料、专家、评分、结论”连成一条清晰的数据链。

这篇文章我会把这个项目从题目拆解、数据库设计、核心技术实现,到部署上线、论文写作、答辩演示的完整过程讲一遍。整体方案谈不上多高深,但贵在业务完整、逻辑清晰,毕设源码、数据库脚本、论文和部署文档这四样东西都能稳稳当当交付出来。

1. 题目拆解:科研项目验收到底在管什么

1.1 这个题目为什么适合作为毕业设计

一个毕业设计题目好不好的标准,我自己的判断是“业务有闭环,技术有难点,工作量可丈量”。

科研项目验收管理系统天然符合这三点。业务上,它有申请、审核、评审、归档,是一条完整的闭环流程;技术上,它有专家分配、评分汇总、状态流转这些值得展开讲的逻辑点,不是无脑的增删改查;工作量上,它涉及的实体足够多,光数据库表就能设计十张以上,论文里光画表和描述流程就能凑够篇幅。

更关键的是,这个题目有现实依据。很多高校的科研管理系统里,验收环节仍然是半手工状态,系统一般只管立项和经费,真正到结题验收时反而要线下折腾。你把它做成毕设,答辩时老师问“你这个系统解决什么问题”,你不需要背概念,直接描述一个真实场景就能打动他。

1.2 业务角色与核心流程

系统里主要靠四类角色撑起整个验收流程:

  • 项目负责人:登录系统后申报项目,提交验收申请,上传结题报告、成果证明、经费使用情况等材料。
  • 科研处管理员:对验收申请做形式审核,确认材料是否齐全、格式是否合规,通过后进入专家评审阶段。
  • 评审专家:查看被分配的项目材料,按指标体系打分,填写评审意见,给出“通过/建议整改/不通过”的结论。
  • 系统管理员:维护用户、角色、专家库、评分指标等基础数据。

流程可以概括成一条主线:项目负责人提交项目信息与验收材料,科研处管理员审核通过后,系统把所有待验收项目归类存档,管理员从专家库中选出与本项目无关的专家,分配评审任务,专家逐一打分,后台按权重汇总平均分,最终由科研处根据得分和专家意见确认项目验收结论。

这条流程里最容易被人忽略的是“过程留痕”。一个项目从申报到最后结项,每一步是谁在什么时间做了什么操作,都应该有日志记录。哪怕论文里不提,这点也会在设计评审表时被老师追问,提前做了能加分。

1.3 功能模块全景

具体功能模块可以拆成下面这张表,论文的功能模块图和开发时的工程结构都按这个来:

模块子功能主要对象
登录认证账号登录、退出、修改密码全部用户
项目管理新增、修改、查询、删除、状态查看项目负责人、科研处
材料管理材料上传、下载、预览、类型分类项目负责人
形式审核审核通过、驳回、填写审核意见科研处
专家库管理专家信息维护、领域维护、回避单位记录系统管理员
专家分配手动分配、自动随机分配、取消重分配科研处
评审打分分指标打分、评审意见、建议决议专家
汇总统计项目通过率、得分排行、项目类型分析科研处、管理员
流程记录状态流转日志、操作日志全部

到这里,题目的业务面基本就清楚了。后面所有设计都是围绕这张表展开的。

2. 技术选型与数据库设计:把业务落到表上

2.1 SpringBoot + Vue + MySQL 为什么是稳妥组合

技术选型不追求新,追求“能自洽、能讲清楚、能装得下业务”。

后端用 SpringBoot,核心原因就是起步快、约定大于配置。SpringBoot 自带 Tomcat、内嵌 JSON 解析、自动化配置,一个spring-boot-starter-web依赖就把 Web 开发的环境全部搞定。配合 MyBatis-Plus,单表 CRUD 几乎不用写 SQL,复杂统计再手写 XML 或注解 SQL,开发效率明显比 SSM 手写一大坨配置高。

前端选 Vue。Vue 的上手曲线比较平缓,用 Vue CLI 搭工程、配好 vue-router 和 vuex,再用 Element UI 把页面组件凑起来,大学生完全可以在两三天内把管理后台的界面撑出来。选 Vue2 + Element UI 是因为它的中文资料最多,遇到问题搜索一下基本都能解决;Vue3 生态虽然更新,但对一个没有太多项目经验的人来说,坑反而更多。

MySQL 则是经典中的经典。InnoDB 支持事务、外键约束、行级锁,一套系统跑几千个项目的数据量毫无压力。字符集记得设置成utf8mb4,存中文姓名、标点符号都不会出乱码。

技术栈评价表如下:

层次技术理由
后端SpringBoot 2.7.x + MyBatis-Plus 3.5.x开发效率高、资料多
前端Vue 2.6.x + Element UI 2.15.x组件全、入门快
数据库MySQL 8.0 + InnoDB + utf8mb4稳定、事务支持好
认证方式JWT + 拦截器轻量、答辩时容易讲细节
部署Nginx + JAR 包结构清晰、资源占用低

2.2 核心表结构与字段设计

数据库是整个项目的地基。我最终设计了十一张表,这里挑最关键的五张说说设计思路。

用户表sys_user:

CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT '登录账号', password VARCHAR(100) NOT NULL COMMENT 'BCrypt加密后的密码', real_name VARCHAR(30) NOT NULL COMMENT '真实姓名', role VARCHAR(20) NOT NULL COMMENT '角色:admin/manager/principal/expert', department VARCHAR(100) DEFAULT NULL COMMENT '所属学院或部门', expert_field VARCHAR(100) DEFAULT NULL COMMENT '研究领域,专家用户才有', status TINYINT DEFAULT 1 COMMENT '1启用 0禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';

这里没有单独拆“角色表”和“用户角色关联表”,因为项目用户只有四种固定身份,拆三张表反而增加了联查复杂度。用角色字段直接区分,在拦截器里判断一下就能完成接口权限控制,属于“够用原则”。

项目表project:

CREATE TABLE project ( id BIGINT PRIMARY KEY AUTO_INCREMENT, project_code VARCHAR(50) NOT NULL COMMENT '项目编号', project_name VARCHAR(200) NOT NULL COMMENT '项目名称', principal_id BIGINT NOT NULL COMMENT '项目负责人ID,关联sys_user', department VARCHAR(100) DEFAULT NULL COMMENT '负责人所属单位', project_type VARCHAR(30) DEFAULT '纵向项目' COMMENT '项目类型', fund_amount DECIMAL(10,2) DEFAULT 0 COMMENT '到账经费', start_date DATE DEFAULT NULL, end_date DATE DEFAULT NULL, apply_status INT DEFAULT 0 COMMENT '0待提交 1待形式审核 2待专家评审 3评审中 4待终审 5已结项 6已退回整改', final_score DECIMAL(5,2) DEFAULT NULL COMMENT '最终验收得分', final_result VARCHAR(20) DEFAULT NULL COMMENT '通过/不通过/建议整改', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科研项目表';

材料表project_material存储每个项目上传的结题材料:

CREATE TABLE project_material ( id BIGINT PRIMARY KEY AUTO_INCREMENT, project_id BIGINT NOT NULL, material_name VARCHAR(200) NOT NULL COMMENT '材料名称', file_path VARCHAR(255) NOT NULL COMMENT '服务器存储相对路径', file_size BIGINT DEFAULT 0 COMMENT '文件大小,字节', file_type VARCHAR(50) DEFAULT NULL COMMENT '扩展名', verify_status INT DEFAULT 0 COMMENT '0未审核 1通过 2被驳回', upload_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目验收材料表';

评审任务表review_task是核心中的核心。一个项目分配给多个专家,就会产生多条评审记录,拿项目 ID 关联:

CREATE TABLE review_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, project_id BIGINT NOT NULL, expert_id BIGINT NOT NULL COMMENT '专家用户ID', score DECIMAL(5,2) DEFAULT NULL COMMENT '专家综合得分', conclusion VARCHAR(20) DEFAULT NULL COMMENT '通过/建议整改/不通过', comment VARCHAR(500) DEFAULT NULL COMMENT '专家意见', submit_time DATETIME DEFAULT NULL, UNIQUE KEY uk_project_expert (project_id, expert_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='专家评审任务表';

再加一张评审明细表review_score,用来存专家对每个指标的打分:

CREATE TABLE review_score ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id BIGINT NOT NULL COMMENT '对应评审任务ID', indicator_id BIGINT NOT NULL COMMENT '指标ID', score DECIMAL(5,2) NOT NULL, UNIQUE KEY uk_task_indicator (task_id, indicator_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评审指标得分表';

指标表accept_indicator单独维护,因为权重和指标名称以后可能会调整,写成死在代码里再改就麻烦了。

2.3 把验收状态抽象成状态机

这系统最值得讲的东西不是 CRUD,而是apply_status字段对应的一整套状态流转。

我在项目里定义七种状态:

  • 0 待提交:项目负责人还在编辑项目信息,可自由修改。
  • 1 待形式审核:负责人点了“提交验收申请”,科研处可以审核。
  • 2 待专家评审:形式审核通过,但专家还没分配。
  • 3 评审中:专家已分配,部分专家已打分,还有专家没提交。
  • 4 待终审:所有专家已打完分,科研处根据汇总结果做最终确认。
  • 5 已结项:终审通过,项目归档。
  • 6 已退回整改:形式审核不通过或终审建议整改,项目回到负责人手里。

状态不是随便跳的,每条转换都有触发动作和操作人。例如:

  • 0 → 1:负责人提交。
  • 1 → 2:科研处形式审核通过。
  • 1 → 6:科研处审核不通过。
  • 2 → 3:自动分配专家后立即进入评审中。
  • 3 → 4:所有专家提交打分完成。
  • 4 → 5:科研处终审通过。
  • 4 → 6:科研处终审建议整改。
  • 6 → 1:负责人整改后重新提交。

每次状态变更都往accept_record表里插一条日志,记录“谁在什么时间把状态从几改成几,备注是什么”。这样管理员在界面上看到的“流程历史”其实就是一个按时间倒序的日志列表,后端实现非常简单,但效果专业很多。

3. 后端核心逻辑实现:登录、上传、分配、评分

3.1 登录认证:JWT + 拦截器,不额外引框架

毕业设计阶段,我不建议把 Spring Security 拉进来。Security 本身是套很完整的权限体系,配置类、过滤器链、UserDetailsService 那一套学起来就能占掉一周时间,而且它帮你做了大部分事,答辩时老师问“过滤器链里面每一步是怎么回事”,你一旦答不上来,反而暴露短板。

自己写 JWT 拦截器,逻辑完全可控,每一个环节都能讲明白。

JWT 生成放在登录接口里,登录成功把用户的 userId、真实姓名、角色塞进 token,设置过期时间,例如 2 小时。自定义拦截器:

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { if ("/api/user/login".equals(request.getRequestURI())) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}"); return false; } token = token.substring(7); try { Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } catch (Exception e) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"token无效\"}"); return false; } } }

拦截器注册时只需要配置一个路径拦截规则,比如/api/**,再放行/api/user/login和文件访问/files/**。

接口级权限我通过自定义注解控制:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { String value(); }

拦截器里在放行前拿到当前方法上的注解,判断角色是否匹配。这个设计比给每个方法手写一次角色判断干净得多,也值得写进论文第五章里。

3.2 材料上传的两个关键点

材料上传最容易被忽略的是配置和路径规划。首先 SpringBoot 默认单文件上传上限是 1MB,实测定不够用。配置文件里要主动调大:

spring: servlet: multipart: max-file-size: 50MB max-request-size: 100MB

文件保存路径我采用了“硬盘目录 + 数据库相对路径”的方式。上传时读取系统临时目录或配置的 uploadPath 属性,按年月建目录,文件名改成UUID.原后缀,避免中文名和重名问题。数据库里只存相对路径,例如/uploads/2025/05/xxxxx.pdf,显示或下载时再拼上服务器地址。

解析上传请求时不要用MultipartFile[]直接一个个入库,先校验扩展名和后缀是否匹配。学聪明一点的做法是:先允许上传,入库后材料状态默认 0(未审核),科研处形式审核时再确认材料。毕设虽然不用做到病毒扫描那么重,但至少要限制 PDF、WORD、ZIP 等常见扩展名,否则一个 exe 传上来演示现场会很难看。

3.3 专家自动分配与回避规则

专家分配是答辩老师最喜欢问的模块,它不像登录那些有现成方案,而是需要自己设计业务规则。

我的规则是这样定的:某个项目进入“待专家评审”状态时,科研处管理员可以一键自动分配。系统先从专家库中筛选出“领域匹配”的专家,即专家档案里的研究领域与项目所属方向一致;然后过滤掉“回避专家”,也就是专家所属学院与项目负责人所属学院相同,防止同事之间相互评审;最后按“当前待评审项目数”升序排列,优先安排手上任务少的专家,取前三位。

关键代码逻辑:

public void autoAssign(List<Long> projectIds) { for (Long projectId : projectIds) { Project project = projectMapper.selectById(projectId); List<User> candidates = expertMapper.selectAvailableExperts(project.getDepartment(), 3); if (candidates.size() < 3) { throw new BizException("项目" + project.getProjectCode() + "可用专家不足3人"); } // 这里是业务核心:批量插入评审任务 List<ReviewTask> tasks = new ArrayList<>(); for (User expert : candidates) { ReviewTask task = new ReviewTask(); task.setProjectId(projectId); task.setExpertId(expert.getId()); tasks.add(task); } reviewTaskMapper.insertBatchSomehow(tasks); project.setApplyStatus(3); projectMapper.updateById(project); } }

这里必须加事务。如果在循环中间专家数量不足,前面项目已经分配了一半,后面报错回滚,整个分配动作会后悔都来不及。我当时的处理是用@Transactional(rollbackFor = Exception.class)包住整个批量分配方法,并在分配之前先一次性校验专家数量,而不是边分配边判断。

3.4 评分汇总与项目结论判定

专家在评审页面填写每个指标的得分,系统实时算出该专家的综合得分。等所有专家都提交后,项目终态得分由系统汇总:

  • 专家综合得分 = Σ(指标得分 × 指标权重)
  • 项目最终得分 = 所有专家综合得分的算术平均

指标权重我放在accept_indicator表的 weight 字段里,比如创新性占 30%、完成度占 30%、成果产出占 25%、经费使用占 15%。这样权重调整时只需要改数据库记录,不需要重新编译后端。

分数和结论判定使用 BigDecimal,不能用 double。double 在求和时会有经典的 0.1 + 0.2 精度问题,一旦答辩演示时算出一个 89.999999 分,自己都会尴尬。

最终结论规则我设置成:

最终得分结论
≥ 75通过
60 ~ 75建议整改
< 60不通过

所有专家都完成打分后,系统自动计算最终得分,调用一个finishReview(projectId)的方法,在同一个事务里完成三件事:更新 project 的最终得分和状态、把所有评审任务的提交时间补上、往流程日志表里插入“评审完成”记录。这三件事必须在一个事务里,因为如果只更新了分数忘了记录日志,流程历史里就会出现“项目状态从评审中变成待终审但没有可追溯记录”的漏洞。

3.5 分页查询与统计接口

列表页需要的不是全表数据,而是“分页 + 多条件筛选”。MyBatis-Plus 的分页插件配置好后,写一个带条件的查询只需一行:

LambdaQueryWrapper<Project> wrapper = Wrappers.lambdaQuery(Project.class) .eq(StringUtils.hasText(name), Project::getProjectName, name) .eq(status != null, Project::getApplyStatus, status) .eq(StringUtils.hasText(department), Project::getDepartment, department) .orderByDesc(Project::getCreateTime); Page<Project> page = projectMapper.selectPage(new Page<>(current, size), wrapper);

统计接口则写了两条自定义 SQL,一条按项目类型分组查数量,一条按月份查验收通过率,返回给前端 ECharts 渲染柱状图和折线图。这类 SQL 不需要复杂优化,数据量在毕业设计范围内,完全跑得动。

4. 前端实现细节:页面、组件与联调

4.1 前端工程结构和路由设计

Vue CLI 生成的前端工程,目录我按职责划分:

src/ ├── api/ // 所有接口请求方法 ├── assets/ // 静态资源 ├── components/ // 通用组件 ├── router/ // 路由配置 ├── store/ // vuex 状态管理 ├── utils/ // axios 封装、token 工具 └── views/ // 页面组件

路由配置上,每个角色看到的菜单不一样。这个项目没有做高度动态化,而是在侧边栏菜单里用v-if判断用户角色来显示对应菜单组。例如“专家评审”菜单只有角色为 expert 才显示。好处是代码简单直观,不容易出错;坏处是权限控制没有做到按钮级别,但对毕设系统来说完全够用。

路由守卫加一道基础校验:

router.beforeEach((to, from, next) => { if (to.path === '/login') { next(); return; } const token = localStorage.getItem('token'); if (!token) { next('/login'); } else { next(); } });

这套方案细致但不过度,答辩演示时如果临时清了缓存被踢回登录页,响应也是符合预期的。

4.2 el-upload 自定义上传处理

Element UI 的 el-upload 组件默认把文件直接 POST 到指定的 action 地址,这在接口需要携带 token、地址又被代理的情况下会很别扭。

我的做法是用http-request属性覆盖默认上传行为,自己用封装好的 axios 上传:

handleCustomUpload(options) { const formData = new FormData(); formData.append('file', options.file); formData.append('projectId', this.projectId); uploadMaterial(formData).then(res => { if (res.code === 200) { this.$message.success('上传成功'); this.loadMaterialList(); } }).catch(() => { this.$message.error('上传失败'); }); }

这样 token 会自动通过 axios 拦截器加到请求头里,后端也能统一校验,比组件自带 action 方式可控得多。另一个容易踩的是:上传成功后要刷新材料列表,不然用户会以为传了个寂寞。

4.3 流程步骤条与状态展示

项目详情页最直观的部分就是流程步骤条。用 Element UI 的 el-steps 把七种状态映射成用户能看懂的四到五个步骤:申报提交、形式审核、专家评审、终审结项。状态映射放在前端工具函数里:

export function statusText(status) { const map = { 0: '待提交', 1: '待形式审核', 2: '待专家评审', 3: '评审中', 4: '待终审', 5: '已结项', 6: '已退回整改' }; return map[status] || '未知'; }

状态变更时重新拉取项目详情,整个步骤条实时更新。这一步是演示时的视觉加分项,能让老师一眼看到系统对流程的控制。

4.4 axios 封装与全局错误处理

前端所有请求都走同一个 axios 实例,拦截器做两件事:请求加 token,响应统一解包和报错。

service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = 'Bearer ' + token; } return config; }); service.interceptors.response.use( response => { const res = response.data; if (res.code === 200) { return res; } if (res.code === 401) { localStorage.removeItem('token'); router.push('/login'); } this.$message.error(res.msg || '请求失败'); return Promise.reject(res); }, error => { this.$message.error('网络异常'); return Promise.reject(error); } );

这个封装让业务代码里不用到处写 try catch,数据流转清晰,后端返回的 code 统一在拦截器处理。开发后期我就是靠这套体系,一遇到 401 就立刻知道 token 出问题,不用一个页面一个页面找。

5. 踩坑实录:开发期间我遇到的七个真实问题

5.1 开发环境的跨域问题

症状:前端页面请求后端接口,浏览器控制台报 CORS error,请求根本没发出去。

原因:开发时前端跑在 8080,后端跑在 9000,浏览器同源策略拦截了跨域请求。

解决方式:开发环境不要在 axios 里写死完整后端地址,而是在vue.config.js里配置 proxy 代理,用相对路径/api发起请求,代理转发到http://localhost:9000。这样前端页面访问的是同源地址,既解决了跨域,也避免了后端接口地址写死在前端代码里。生产环境则由 Nginx 提供反向代理,同样只暴露一个域名。

5.2 时间字段和时区问题

症状:前端展示的后台时间比实际时间早 8 小时,数据库里存的时间没问题,但 JSON 返回时被转成了 UTC 格式。

原因:SpringBoot 默认处理 LocalDateTime 时没有指定时区,Jackson 序列化用的是默认 UTC。

解决方式:在 application.yml 里统一配置:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

同时 MySQL 连接串加上serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8。这两处配置缺一个,时间就会在整个链路某一段发生偏移。这种问题在本地测不出,部署到国内服务器后如果时区不对,马上会暴露。

5.3 JWT 过期导致重复跳转

症状:token 过期后,页面同时发出多个请求,全部返回 401,结果响应拦截器里的router.push('/login')被调用了好多次,浏览器里路由报错。

原因:拦截器里没有做“是否已跳转登录页”的判断。

解决方式:在拦截器外层用一个标志位控制,如果已经触发过一次 401 跳转,后续请求就不再重复跳:

let isRedirecting = false; if (res.code === 401) { if (!isRedirecting) { isRedirecting = true; localStorage.removeItem('token'); router.push('/login'); } }

这个小细节虽然不起眼,但演示时候如果 token 恰好过期,系统不会出现一堆重复跳转的奇怪现象。

5.4 上传文件大小限制

症状:上传一个十几兆的 PDF 时报错,请求会在进入 Controller 之前就被 Tomcat 拒掉。

原因:SpringBoot 默认单文件上传上限是 1MB,同时如果部署了 Nginx,Nginx 的默认请求体上限也是 1MB。

解决方式:两处都要改。后端在 application.yml 中调大 multipart 限制,Nginx 配置中加:

client_max_body_size 100m;

只改后端不改 Nginx,部署到线上照样失败。我第一次就只改了后端,本地测试通过,部署后被打了个措手不及。

5.5 文件下载中文名乱码

症状:下载验收材料时,文件名带中文的文件在部分浏览器里变成一串乱码。

原因:Content-Disposition响应头里的 filename 是 ISO-8859-1 编码,中文没有正确编码。

解决方式:后端下载接口设置响应头时使用 RFC 5987 格式:

String encodedName = URLEncoder.encode(realName, "UTF-8").replaceAll("\\+", "%20"); response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedName);

实际项目中,我是先查数据库拿到存储的文件名,再返回给前端旧文件名。后端存储名用 UUID,但下载时展示的还是用户上传时的原始名字,这个体验不能省。

5.6 前端刷新页面 404

症状:部署后,用户在一个详情页刷新,Nginx 直接返回 404。

原因:Vue Router 使用了 history 模式,浏览器直接访问某个具体路由时,Nginx 没有对应的真实物理文件,所以找不到。

解决方式:在 Nginx 配置一个 try_files:

location / { try_files $uri $uri/ /index.html; }

这样所有路径都先找静态文件,找不到就回退到 index.html,由前端路由接管。这个配置几乎每个 Vue 部署项目都会遇到,属于必考项。

5.7 MyBatis-Plus 字段自动填充不生效

症状:实体类里明明加了@TableField(fill = FieldFill.INSERT_UPDATE),但更新数据时 update_time 字段没变化。

原因:自动填充需要一个实现类去真正执行赋值逻辑,加了注解不代表框架会自动找到值从哪来。

解决方式:定义一个实现类,在 insertFill 和 updateFill 方法里填充当前时间:

@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }

如果连这套都不想做,最省事的办法就是像项目表里那样用数据库层的ON UPDATE CURRENT_TIMESTAMP,前提是数据库自己管理更新时间,不依赖业务代码。

6. 论文、部署文档和答辩展示的实战经验

6.1 论文怎么组织结构

毕设论文一般六到七章,我的参考结构是:

  • 第一章 绪论:写科研项目验收的背景和意义,列出国内外管理系统研究现状,再说明本文主要工作。
  • 第二章 相关技术介绍:SpringBoot、Vue、MySQL、MyBatis-Plus、JWT。不需要长篇大论,每种技术一小节,两三页就够。
  • 第三章 系统分析:角色分析、业务流程、功能需求、非功能需求。这里要把“验收流程”画成流程图,把每个用例写清。
  • 第四章 系统设计:总体架构图、功能模块图、数据库 E-R 图、核心表结构。
  • 第五章 系统实现:环境说明、核心模块实现过程,配关键代码截图和界面效果图。重点放专家分配、评分汇总、状态流转。
  • 第六章 系统测试:功能测试用例表 + 性能测试数据。

论文里要注意一个原则:图和表永远比文字重要。E-R 图、用例图、流程图、系统架构图,这些图一摆,页数瞬间厚重起来,老师也更容易看懂你的系统。

6.2 部署文档怎么写才能直接照做

部署文档一定要按“从零开始”的思路写,而不是只写你自己服务器的操作记录。我总结的部署文档结构:

  1. 环境要求:JDK 1.8+,MySQL 8.0,Nginx 1.2+,Node.js 16。
  2. 数据库初始化:执行db_init.sql,脚本里包含建库、建表、初始数据。管理员和测试账号的密码统一在SQL里注明明文和加密后值。
  3. 后端部署:mvn clean package -DskipTests打成 jar,用下面的命令启动:
java -jar acceptance-system.jar --server.port=9000 --spring.profiles.active=prod
  1. 前端部署:npm run build生成 dist,把 dist 里所有文件拷贝到 Nginx 的 html 目录。
  2. Nginx 配置:转发/api到后端 9000 端口,转发/files到本地磁盘路径,其余路径指到前端静态文件。
  3. 启动顺序和验证:先启动 MySQL,再启动 jar,最后启动 Nginx。浏览器访问首页,能登录成功就算部署完成。

部署文档里一定要写清楚“端口是否被占用”“如何看日志”“如何重启”,这是最容易忽略但实际运维里最先用到的三件事。

6.3 答辩演示路径与常见问题应对

答辩演示不要一上来就摆技术,先按业务角色走一遍。我的演示动线是:

  1. 用项目负责人账号登录,新建一个“基于深度学习的图像识别系统横向课题”项目,上传一份结题报告 PDF。
  2. 切换到科研处账号,进行形式审核,审核通过后进入专家分配,点击“自动分配”,演示系统选出三位本领域专家。
  3. 切换到专家账号,打开两个待评审项目,分别填写指标分数和评审意见,提交。
  4. 回到科研处,查看项目状态已变为“待终审”,最终得分 82.5 分,点击“确认通过”,项目结项。
  5. 打开统计页面,看项目类型分布图和验收通过率折线图。

这样演示下来,所有核心功能都覆盖了。老师最后提问主要集中在几个点:状态是怎么流转的、专家分配规则是什么、为什么不引入 Spring Security、多专家打分时数据库怎么防止重复提交。回答的核心思路永远是从业务需求出发:状态流转因为有状态机字段,专家分配因为有回避规则,不引入 Security 是因为自定义 JWT 拦截器够用且在论文里能讲清细节。只要自己的逻辑是自洽的,老师们一般不会死磕某一个框架。

最后再分享一个小技巧:整个项目编码过程我给自己定了一条纪律,每个接口提交前都要先想“如果数据是脏的、并发是高的,这个接口会不会崩”。按这条标准去自查一遍,能少踩很多并发和空指针的坑。答辩时也把这个过程讲出来,比干背知识点要有说服力得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询