SpringBoot+Vue大学生创新创业项目管理系统设计与实现

发布时间:2026/10/11 12:06:20
SpringBoot+Vue大学生创新创业项目管理系统设计与实现
一个高校里负责创新创业项目管理的人一年往往要经手几百份项目申报书、中期检查表、结题报告从收集材料到组织评审再到汇总成绩和经费拨付整个流程全靠表格和群消息来撑。我见过有人连续加班一周就是为了在Excel里把不同格式的申报书信息重新录一遍也见过评审专家抱着厚厚一叠纸质材料往返奔波打分还要手工统计加计算既费时又容易出错。这套“基于SpringBootVue的大学生创新创业项目管理系统”就是冲着这些痛点去的。它把学生申报、指导教师审核、学院审批、校级评审、立项、中期检查、结题验收、成果归档、数据统计这一整套业务流程搬到了线上数据存进MySQL接口由SpringBoot提供页面则用Vue来渲染。适合两类人拿来学习和复用一类是正在做毕业设计或课程设计需要一套完整可跑的项目作为参考的学生另一类是学校或二级学院里真正需要给创新创业项目管理降本增效的老师、教务人员拿这套代码改一改就能部署。下面我会把整套系统的设计思路、数据库结构、后端接口实现、前端页面搭建、部署上线的完整过程都拆开讲一遍包括那些开发文档里不会写的坑。1. 项目到底在解决什么问题业务流程先想清楚再写代码刚开始做这类系统的人最容易犯的错是一上来就建表写接口结果做着做着发现某个角色需要的功能没考虑进去又回头改表结构后面全部跟着返工。所以先别碰代码把业务流程画明白。1.1 创新创业项目管理背后的真实痛点以某高校的实际场景为例创新创业项目的全生命周期大概是这样的学校发申报通知学生准备申报书导师先审核一遍选题是否可行、方向有没有价值然后交到二级学院初筛再由校级层面组织专家评审评审分数高的项目正式立项。立项之后进入实施期隔一段时间要交中期检查报告项目快结束时还要准备结题材料专家验收通过才能正式结项最后还有成果统计、经费报账等信息要汇总存档。原来这套流程是怎么跑的无非是学生把Word申报书发到邮箱或微信老师手动整理文件名、编号、归类到文件夹通知靠群消息层层转发容易漏审核意见靠截图或语音传话没有留痕评审打分用Excel专家之间互相看不到汇总的时候还得人工去重、算平均分结果公示靠打印张贴学生查询体验很差。这套系统把上述场景完全数字化之后核心价值其实就三条一是所有环节都在线留痕谁在什么时间做了什么操作有日志可查二是数据一处录入多处复用项目编号、负责人、经费、成果这些字段不用重复填三是评审和统计交给系统自动处理专家在线打分系统算均分聚合成报表把事务性工作时间大幅压缩。1.2 系统角色权限与业务流程闭环整体流程梳理清楚后角色也就自然出来了每个角色对应的职责和痛点都不相同角色典型操作旧流程痛点系统解决方案学生申报项目、上传材料、查看进度不知道材料交到哪个环节状态实时可视待办提醒指导教师审核选题、填写指导意见意见分散在聊天记录里在线审核、意见留痕二级学院管理员初筛项目、分配评审专家手动统计本院申报数据本院数据聚合、一键上报校级管理员发布通知、组织评审、立项公示、数据汇总Excel反复合并、人为误差评审自动算分、统计报表自动生成要注意的是一个学生在一个年度里通常只能担任一个项目的负责人但可以参与多个项目一个老师也可以指导多个项目。这种一对多的关系在后面对表结构设计影响很大。业务流程我习惯画成一条线性状态机来理解这也是后来数据库里status字段的核心依据草稿 - 待指导教师审核 - 待学院审核 - 待校级评审 - 已立项 - 中期检查 - 待结题 - 已结题任何一个审核环节不通过直接退回草稿或退回上一级并附带退回意见。这么设计最大好处是任何角色打开系统只看一眼状态就知道这个项目卡在谁那里。2. 技术选型为什么是这套组合SpringBoot、Vue、MySQL、MyBatis很多人在技术选型上容易犯选择困难症。这里直接说结论对于“中小规模、多角色、前后端分离、需要快速交付的管理系统”SpringBoot Vue MySQL MyBatis 这套组合到今天依然是最稳的方案没有之一。2.1 后端选择SpringBoot的理由SpringBoot现在基本是Java后端开发的事实标准。它最核心的价值在于“自动配置”把原来SSH时代需要手动做的Bean装配、事务管理、数据源配置全部接管了。开发的时候只需要在pom.xml引入依赖配上application.yml就能快速启动一个内嵌Tomcat的Web应用不用再去独立部署容器。更重要的是生态成熟。做用户认证有Spring Security和JWT的现成方案做参数校验有Hibernate Validator做接口文档有Swagger/Knife4j做定时任务有Scheduled注解直接用。遇到问题搜索引擎上能翻到大量同类案例这一点对于学生项目来说非常关键因为不可能指望一个团队都精通底层原理更多时候是现查现用。我用SpringBoot还有个很私人的理由它的启动器命名规范太好了。看到spring-boot-starter-web就知道是MVC相关的看到mybatis-spring-boot-starter就知道是数据库映射的依赖管理非常清晰不容易出现莫名其妙的版本冲突。2.2 ORM选型MyBatis而不是JPA数据库访问层我选了MyBatis没有用Spring Data JPA。不是说JPA不好而是在这类管理系统中MyBatis有两点天然优势。第一SQL完全自己控制。创新创业项目管理系统里有大量复杂查询比如按项目名称、当前状态、所属学院、项目类别、负责人姓名这些条件任意组合的分页查询MyBatis的动态SQL写起来特别直观一个if标签就能搞定SQL长什么样完全可控。JPA虽然也能通过Specification实现但用起来抽象规则一多生成的SQL性能出问题还不容易排查。第二MyBatis的字段映射非常透明。数据库字段叫project_name实体类属性是projectName配置文件里写清楚映射关系即可不会像Hibernate那样有一堆隐式约定。团队成员也好上手只要会写SQLMyBatis没有额外学习成本。当然MyBatis也有坑后面在常见问题里会单独讲。2.3 前端选Vue和Element UI的原因前端的核心诉求是多角色后台管理、表单较多、表格密集需要快速搭出专业感强的页面。Vue的双向绑定机制天然适合表单场景用户在页面填一个申报资料数据实时同步到state里提交一次请求就完成。配套的Element UI组件库更是管理后台的利器el-table解决了表格展示分页列排序el-form自带校验规则el-upload封装了文件上传el-tabs让我能够把“项目基本信息、成员信息、附件材料、历史审批记录”放在同一个详情页面切换查看。整套UI做出来视觉风格统一交互细节也基本达标不用自己造轮子。路由我用Vue Router状态管理用了Vuex请求封装用Axios。这四个组合几乎成了Vue管理后台的固定配方无论是学习还是维护都很有底气。2.4 数据库为什么是MySQLMySQL对中小系统的适配度实在太高了。开源免费InnoDB存储引擎支持事务和行级锁单表千万级数据在有索引和合理SQL的情况下也不会出大问题。而创新创业项目管理系统一个学校一年撑死几千个项目数据量是百万级以内的量级MySQL的表现绰绰有余。选择MySQL还有一个隐性好处部署方便不管本机还是服务器装好服务端就能启动运维资料浩如烟海基本所有问题都能查到现成解决方案。相比之下换商用数据库不仅增加成本还会让整个项目组的学习曲线陡增。对于这套系统来说MySQL是最务实的决策。3. 数据库设计一张表一张表地拆清楚表结构是整个系统的地基地基打歪了后面写代码必返工。我把这套系统的核心表设计思路完整贴出来并且把为什么要这么设计讲明白。3.1 用户表如何支撑三个角色的权限区分用户表是系统的第一张表所有业务都围绕人展开。CREATE TABLE sys_user ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 用户ID, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(255) NOT NULL COMMENT 密码(Bcrypt加密), real_name varchar(50) NOT NULL COMMENT 真实姓名, role tinyint(4) NOT NULL COMMENT 角色:0学生 1指导教师 2学院管理员 3校级管理员, college_id int(11) DEFAULT NULL COMMENT 所属学院ID, phone varchar(20) DEFAULT NULL COMMENT 手机号, email varchar(100) DEFAULT NULL COMMENT 邮箱, status tinyint(4) DEFAULT 1 COMMENT 状态:0禁用 1正常, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;这里用role字段做简单角色区分没有引入RBAC体系因为这套系统的权限相对固定只有四种角色功能菜单是确定性的。如果用完整的用户-角色-权限五张表反而会大幅提高学习成本。数据库设计要区分“需要通用性”和“需要简单可靠”这属于后者。密码字段存的是密文用BCrypt加密。实际测试下来Bcrypt算法对同一明文每次生成的密文都不一样自带盐值比MD5简单拼接安全得多。用户表另一个关键点是用username做唯一索引不允许账号重复而real_name才是用于展示的姓名。3.2 项目管理主表状态机字段设计项目表是业务的核心几乎所有主流程的数据最后都汇到这里。CREATE TABLE project ( id int(11) NOT NULL AUTO_INCREMENT, project_code varchar(50) DEFAULT NULL COMMENT 项目编号, project_name varchar(200) NOT NULL COMMENT 项目名称, category varchar(50) DEFAULT NULL COMMENT 项目类别:创新训练/创业训练/创业实践, level varchar(20) DEFAULT NULL COMMENT 级别:国家级/省级/校级, leader_id int(11) NOT NULL COMMENT 项目负责人(学生用户ID), advisor_id int(11) DEFAULT NULL COMMENT 指导教师ID, college_id int(11) DEFAULT NULL COMMENT 所属学院ID, budget decimal(10,2) DEFAULT NULL COMMENT 项目经费, start_time date DEFAULT NULL COMMENT 开始时间, end_time date DEFAULT NULL COMMENT 结束时间, summary text COMMENT 项目简介, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态:0草稿 1待导师审核 2待学院审核 3待校级评审 4已立项 5中期检查 6待结题验收 7已结题 8已退回, apply_file varchar(255) DEFAULT NULL COMMENT 申报书文件路径, midterm_file varchar(255) DEFAULT NULL COMMENT 中期检查文件路径, final_file varchar(255) DEFAULT NULL COMMENT 结题报告文件路径, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_project_code (project_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT项目表;status字段是整个业务流程的开关我用了1~8的数字状态去管理流转。为什么不用字符串因为数字排序、比较和前端映射枚举都更方便查询status4直接是立项项目走索引效率也更高。project_code是项目编号生成规则通常采用“年份学院代码序号”比如2024JSJ001。这个编号要唯一因为项目正式立项后要对应经费和成果编号就是项目的身份证。注意这里的create_time用数据库默认值自动生成插入时不需要手动赋值。3.3 项目成员表与评审表多对多关系的处理一个项目有多名成员一个学生可以参加多个项目这是典型的多对多关系必须拆出第三张关联表不能把成员字段塞在项目表里。CREATE TABLE project_member ( id int(11) NOT NULL AUTO_INCREMENT, project_id int(11) NOT NULL COMMENT 项目ID, user_id int(11) NOT NULL COMMENT 成员用户ID, role_in_project varchar(20) DEFAULT NULL COMMENT 成员角色:负责人/成员, join_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_project_user (project_id, user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT项目成员表;这里加了唯一联合索引(project_id, user_id)防止同一个人重复加入同一个项目。业务上还有一个隐性规则同一项目中只能有一个负责人。这个通过前端表单控制即可因为涉及并发修改的概率很低数据库不用做复杂约束。评审表用于校级评审环节多个专家对多个项目评分也是多对多关系CREATE TABLE project_review ( id int(11) NOT NULL AUTO_INCREMENT, project_id int(11) NOT NULL, reviewer_id int(11) NOT NULL COMMENT 评审专家(用户ID), score decimal(5,2) DEFAULT NULL COMMENT 评分(百分制), comment varchar(500) DEFAULT NULL COMMENT 评审意见, review_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_project_reviewer (project_id, reviewer_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT项目评审表;评审表的唯一约束(project_id, reviewer_id)确保了同一个专家不能对一个项目重复打分这个约束是业务刚需必须有数据库兜底。项目最终得分由系统实时计算多个专家评分的平均值这个计算在Service层完成也可以用SQL完成我建议在Service层算逻辑更清晰调试也方便。4. 前后端分离下的认证与接口后端核心实现拆解表设计好之后后端开发就有了清晰的地图。这一节我挑几个最核心、也最容易绕弯的实现细节展开讲。4.1 JWT登录认证不做Session的原因早期JavaWeb项目常用Session把用户状态存在服务端但前后端分离之后前端页面和后端接口可能部署在不同域名、不同端口甚至前端是纯静态资源后端是独立服务Session的跨域问题非常麻烦管理也复杂。我在这套系统里用JWT做无状态登录。整体流程是用户提交账号密码后端校验通过后用用户ID和角色生成一段Token字符串返回前端把Token存在localStorage里每次请求在请求头带上Authorization字段后端在所有需要登录的接口上拦截解析并校验Token校验通过才放行。加点关键代码帮助理解这是Token工具类的核心逻辑public String createToken(Long userId, Integer role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public Claims parseToken(String token) { try { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); } catch (ExpiredJwtException e) { throw new BusinessException(登录已过期请重新登录); } catch (JwtException e) { throw new BusinessException(无效的登录凭证); } }解析Token的时候要分别捕获过期和非法两种情况前端才能根据提示跳转登录页而不是显示一串乱码。实际开发里我把这段封装在JwtInterceptor拦截器里注册拦截器时排除登录接口和静态资源路径其余接口一律先校验Token省去在每个Controller里重复写鉴权代码。Token过期时间这里有个权衡。时间太短比如30分钟会导致用户做几个操作就得重新登录体验很差时间太长比如7天则一旦Token泄露风险大。这个系统属于校内工具型应用安全级别中等我设置的过期时间是2小时配合前端在收到401响应码时的自动跳转登录页逻辑体验还算流畅。4.2 项目分页查询MyBatis动态SQL与参数传递创新创业项目管理系统最普遍的一个操作就属项目管理列表页的查询。用户在前端选一个状态、填一个项目名称关键词点查询后端就要返回符合条件的项目列表还要支持分页。这个功能用MyBatis的动态SQL实现是最能体现MyBatis好处的场景。Mapper里的SQL大概是这样select idselectProjectPage resultTypecom.demo.vo.ProjectVO SELECT p.id, p.project_code, p.project_name, p.category, p.budget, p.status, p.create_time, u1.real_name AS leader_name, u2.real_name AS advisor_name, c.college_name FROM project p LEFT JOIN sys_user u1 ON p.leader_id u1.id LEFT JOIN sys_user u2 ON p.advisor_id u2.id LEFT JOIN sys_college c ON p.college_id c.id where if testprojectName ! null and projectName ! AND p.project_name LIKE CONCAT(%, #{projectName}, %) /if if teststatus ! null AND p.status #{status} /if if testcategory ! null and category ! AND p.category #{category} /if if testcollegeId ! null AND p.college_id #{collegeId} /if /where ORDER BY p.create_time DESC if testoffset ! null and pageSize ! null LIMIT #{offset}, #{pageSize} /if /select注意几个细节第一连接查询用LEFT JOIN因为一个项目可能还没分配指导教师直接用INNER JOIN会把未分配老师的记录过滤掉这是新手最容易踩的坑。第二模糊查询不要写LIKE %#{projectName}%MyBatis解析会报错正确写法是CONCAT拼字符串。第三LIMIT的offset值在Service层计算offset (pageNum - 1) * pageSize前端只需要传当前页码和每页条数。有的同学喜欢用PageHelper插件来自动分页我也用过。PageHelper的好处是不用手动算offset拦截器自动在SQL后面拼LIMIT但有个致命要求PageHelper.startPage()和Mapper查询必须处于同一个线程和同一个方法调用链中中间不能穿插其他Mapper方法。我实际改造的时候遇到过分页总数错误的情况查半天发现是startPage之后先调了另一个查询。对于这种中小系统我反而更推荐手动分页代码虽然多几行但排查问题一目了然。另外一个角色的权限过滤也要在这里考虑如果登录的是学院管理员那查询条件必须强制加上college_id当前用户所属学院否则学院管理员就能看到全校项目数据了。这个过滤字段不要由前端传而是在Controller从Token解析出当前用户ID后去用户表查出college_id赋给查询对象从源头上堵住越权。4.3 文件上传与下载申报书、结题报告存到哪里创新创业项目管理系统里大量的申报书、中期检查报告、结题材料都是文件文件存储方案必须想清楚。实践中最简单的方案是把文件存在本地磁盘目录数据库里只保存相对路径。比如配置一个上传目录upload/folder/2024/xxx.pdf数据库字段apply_file存upload/folder/2024/xxx.pdf接口返回给前端时再拼接成完整访问URL。这种方案的优点是实现简单、备份直观适合并发量不高的系统。文件上传的Controller方法核心逻辑PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(上传文件不能为空); } // 限制文件类型和大小 String originalName file.getOriginalFilename(); long fileSize file.getSize(); if (fileSize MAX_FILE_SIZE) { return Result.error(文件大小超出限制最大50MB); } // 按业务类型分目录存储 String typeDir upload/apply/; String newFileName UUID.randomUUID().toString() getExtension(originalName); String absolutePath BASE_PATH typeDir newFileName; File dest new File(absolutePath); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } file.transferTo(dest); return Result.success(typeDir newFileName); }文件按业务场景分开存储申报书、中期、结题便于管理但要注意文件名一定要重命名成UUID否则两个学生上传同名的申报书就会互相覆盖这是血泪教训。还有一个重点是granted URL静态资源映射否则上传成功但浏览器访问不了文件。在SpringBoot配置类里加一个资源映射Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${file.upload-path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceLocations(file: uploadPath); } }这里file:后面的路径末尾一定保留斜杠路径拼接错误会导致浏览器无法预览PDF。上传的时候我还会顺手校验一下MultipartFile的contentType拒绝exe、jsp这类危险类型的文件防止有人上传木马文件这个安全细节千万别省。5. 前端页面搭建与交互逻辑怎么把界面做得又好用又好看后端的接口有了前端就是把接口对接到页面上把流程渲染出来。这一节不是贴完整代码而是把管理后台前端最核心的几个设计逻辑讲明白。5.1 前端权限控制路由守卫与菜单动态渲染不同角色登录进来看到的菜单和页面不同这个靠前端路由meta和Vue Router前置守卫实现。const router new VueRouter({ routes: [ { path: /login, component: Login }, { path: /dashboard, component: Layout, children: [ { path: project/list, component: ProjectList, meta: { roles: [0, 1, 2, 3], title: 项目管理 } }, { path: review/manage, component: ReviewManage, meta: { roles: [3], title: 评审管理 } }, { path: statistics, component: Statistics, meta: { roles: [2, 3], title: 数据统计 } } ] } ] }) router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) return next() if (!token) return next(/login) const role Number(localStorage.getItem(role)) if (to.meta.roles !to.meta.roles.includes(role)) { return next(/403) // 无权限页面 } next() })路由守卫的好处是无权限的人就算手动修改URL也进不去对应页面。但要注意前端路由守卫只是用户体验层面真正安全的权限控制必须依赖后端接口的鉴权。我在后端所有敏感接口上都根据Token中的角色做了拦截判断防止有人绕过前端界面直接调接口。两层防护缺一不可。菜单动态渲染的做法比较简单在侧边栏组件里根据当前role变量用v-if控制显示哪一组菜单不需要做复杂的分角色菜单表。因为角色只有四类硬编码最直接。5.2 项目申报表单与审核页面的交互设计前端页面里最复杂的表单是项目申报。这个表单要填项目名称、类别、经费预算、项目简介还要维护多成员列表和上传申报书文件。成员列表用el-form中动态增减的字段组实现每条成员数据是一行输入框点“添加成员”往数组里push一条点“删除”splice掉整体数据直接绑在一个members数组上提交时和项目基本信息一起发送。表单校验不要等到后端报错才提示前端先用规则拦一道。比如项目名称必填、预算必须大于0、成员至少1人这些用Element UI的rules规则就能完成rules: { projectName: [{ required: true, message: 请输入项目名称, trigger: blur }], category: [{ required: true, message: 请选择项目类别, trigger: change }], budget: [{ required: true, type: number, min: 0, message: 预算必须大于0, trigger: blur }] }审核页面核心是状态操作按钮和意见填写。指导教师审核时可以点通过或退回退回时意见框必填通过时意见选填。这个必填逻辑不能只在后端判断前端也要加因为用户习惯是看到表单没报错就提交后端报了错又要返回来改体验不好。还有一个容易被忽略的细节审核列表要用tabs或下拉框区分当前项目状态。比如指导教师登录后看到“待我审核”和“我已审核”两个列表这样才能快速聚焦待办。状态筛选本身就是分页查询条件把status作为参数传给后端后端动态SQL直接过滤。5.3 数据统计图表用ECharts把申报数据可视化等到数据积累到一定量统计功能的价值就体现出来了。校领导想知道今年全校申报了多少项目、每个学院分布如何、各类别项目占比多少靠看表格不直观用ECharts图表一眼就能看明白。统计接口在聚合数据后返回给前端的是标准JSON数组。例如按学院统计申报项目数量返回[ { name: 计算机学院, value: 42 }, { name: 机械工程学院, value: 38 }, { name: 经济管理学院, value: 29 } ]前端拿到数据配置ECharts的饼图series和xAxis渲染即可。项目中实现统计报表用最多的无非是三类图饼图展示各类别比例或状态分布柱状图展示各学院申报数量折线图展示近五年立项数量变化趋势。有人会把统计的SQL写得很复杂比如在项目表里GROUP BY college_id再关联学院表。实际上我更推荐后端写专用统计SQL时尽量保持轻量只查维度和指标两个字段避免group by加多个join导致索引失效。数据量不大时这种简单查询性能完全够用代码可读性也高很多。6. 部署上线与常见问题排查实测中踩过的那些坑开发环境跑通只是第一步。系统真正能交出去用部署上线和问题排查才是实战。这一节我把在项目中遇到的典型问题记录成速查表方便大家直接参考。6.1 Nginx部署前后端分离项目的完整过程开发时前端由Node服务托管后端由SpringBoot内嵌Tomcat托管端口不同跨域由代理解决。但正式部署时需要一个统一入口我的方案是后端打jar包跑在8080端口前端执行npm run build生成静态资源dist目录然后由Nginx统一监听80端口静态资源由Nginx直接托管接口反向代理到Java后端。Nginx核心配置server { listen 80; server_name your-domain.com; root /opt/project/dist; index index.html; # 前端路由history模式刷新404问题 location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 上传文件访问 location /files/ { proxy_pass http://127.0.0.1:8080/files/; } }几个必须说清楚的细节第一前后端分离项目用Vue Router的history模式时刷新页面会出现404必须配try_files $uri $uri/ /index.html第二proxy_pass http://127.0.0.1:8080/ 末尾的斜杠不能丢它表示把/api/前缀去掉后转发到后端否则后端收到的URL就还是带/api/路由匹配不上第三如果用了history模式后端Controller跨域配置和Nginx转发要在同一套URL规则下不要前端口径不一致。6.2 常见问题速查表故障现象根本原因解决方案前端请求接口报跨域错误开发端口与后端端口不一致未配置代理开发期前端配置server.proxy代理到后端或后端配置CORS过滤器刷新页面404Vue Router history模式部署在Nginx未配置回退配置try_files $uri $uri/ /index.html登录后请求接口返回401Token过期或解析失败检查后端系统时间是否有偏差前端拦截401跳转登录页MySQL查询中文乱码数据库连接URL未指定utf8字符集jdbc URL加characterEncodingutf8useUnicodetrue表编码改成utf8mb4分页数据记录数与总数不一致PageHelper.startPage使用位置错误将startPage紧挨在Mapper查询之前调用中间不能有别的查询上传大文件直接连接中断SpringBoot默认文件大小限制1MB在配置中增加multipart.max-file-size与max-request-size上传文件访问403本地存储路径与静态资源映射不对核对addResourceLocations的file路径与配置文件上传路径一致项目状态流转错乱未做状态判断任何角色都可修改在Service层根据状态校验当前操作是否被允许不符合直接抛异常最值得单独提示的是第2条“前端刷新404”十个部署前后端分离项目的人起码有六个会卡在这一步。原因很简单Vue Router history模式把路由切换交给了前端js但浏览器刷新时直接请求的是一个真实路径Nginx按目录找文件自然找不到。除非你对Nginx配置非常熟否则排查思路就是看刷新后浏览器请求的URL如果它请求的是一个html不存在的路径把它try_files到index.html即可。6.3 关于这套系统还能怎么扩展既然源码完整未来需要继续演进的方向也比较清晰。权限体系可以升级成RBAC模型引入角色表和菜单表方便后面扩展更多角色文件存储可以接入云存储把上传文件从中转磁盘迁到对象存储评审打分支持更多维度比如创新性、可行性、预期成果分别打分再加专家权重消息通知可以接入邮箱或企业微信把“项目已通过审核”“被退回”这种状态变化主动推送给相关人。不过我特别想说的是扩展功能之前务必先把现有的代码吃透。这套系统的业务主线是比较清爽的如果一开始就想把所有扩展都塞进来项目复杂度会上升很快反而失去作为学习范例的价值。回头看我做这个项目的整个过程最深刻的体会是很多坑并非技术难而是流程和细节没有想清楚就动手。状态字段的取值统一了吗审核的退回逻辑边界清楚吗文件重命名做了吗越级查询堵住了吗这些问题每一个单拎出来都很简单但合在一起结果就会天差地别。希望这套系统的完整拆解能帮你在动手之前少走几段弯路。