Spring Boot + Vue 全栈实战:蘑菇百科信息管理系统开发详解 1. 项目定位与核心功能拆解1.1 这个蘑菇百科到底能做什么先把这个项目说清楚。所谓“蘑菇百科”本质是一个面向科普场景的蘑菇信息检索与管理系统。它解决的实际问题很朴素蘑菇种类太多、外观相似度又高光靠翻图鉴或者问人效率太低。我参与这个项目时需求方是某自然科普团队他们手里攒了大量野外采集的蘑菇照片和特征记录之前散落在表格和文件夹里检索全靠记忆做科普活动时找素材特别费劲。所以当时定下的目标很简单做一个能查、能看、能管、能分享蘑菇信息的系统。这个系统最核心的价值在于把零散的蘑菇资料结构化。管理员可以在后台录入蘑菇的学名、别名、形态特征、生长环境、分布区域、毒性说明、食用建议等字段前台用户则可以通过分类浏览、关键词搜索、甚至按季节或生境条件筛选找到自己想了解的蘑菇。搜索结果直接展示图文卡片点进去就是详情页附带图片轮播和相关品种推荐。从技术训练的角度看这个项目也非常适合拿来练手。它几乎覆盖了一门全栈课程最常考的所有知识点Spring Boot 的 REST API 设计、MyBatis Plus 的 CRUD 和条件构造器、Vue 组件化开发、Axios 前后端联调、JWT 登录鉴权、文件上传与静态资源映射、以及最基础但最容易翻车的跨域配置。对正在找工作、需要写进简历的开发者来说一个能完整跑通的“蘑菇百科”项目比十个只写了登录注册的 Demo 都有说服力。如果你是想自学 Spring Boot 和 Vue拿这个项目作为第二个或第三个完整的实战项目也很合适——功能规模不大不小单人完成大概需要一到两周正好卡在“学到东西”和“不会烂尾”之间。1.2 功能模块地图与用户角色系统按使用角色分成两条线普通用户和管理员。普通用户不需要登录就能浏览蘑菇百科内容这样降低了科普访问门槛但如果想收藏蘑菇、发表评论就需要注册登录。管理员则负责内容维护包括蘑菇信息的新增、修改、删除分类管理以及用户评论的审核清理。具体的模块划分我梳理成下面这张表模块子功能说明用户模块注册、登录、个人信息JWT 鉴权密码 MD5 加盐存储蘑菇模块蘑菇列表、搜索、详情、分类筛选分页展示支持组合条件查询收藏模块收藏、取消收藏、收藏列表用户维度隔离防止越权操作评论模块评论发布、评论列表、删除管理员可删用户只能删自己的分类模块分类维护、分类树层级分类例如“担子菌门/伞菌纲/伞菌目”图片模块图片上传、图片展示本地磁盘存储 URL 映射数据统计模块蘑菇总数、分类统计、访问量首页报表用 ECharts 展示柱状图实际录入数据的时候建议按“常见食用菌 常见毒菌”两条线并行填充。比如食用菌里放香菇、平菇、金针菇毒菌里放毒鹅膏、白毒伞、墨汁鬼伞。这样既能展示项目的能力边界也让科普内容有对比价值——毕竟普通用户最关心的就是“这个蘑菇能不能吃”。我当时录入首批数据时特意把每一种蘑菇的“可食性”和“毒性”字段都填完整这直接关系到底部筛选功能是否好用。2. 技术选型与架构设计2.1 后端为什么要选择 Spring Boot MyBatis Plus后端选 Spring Boot 几乎不需要犹豫。一来生态成熟二来招聘市场需求量大。Spring Boot 2.6 是目前稳定性和资料丰富度都比较平衡的版本避免一上来就踩 Spring Boot 3.x 的 Jakarta 命名空间迁移坑。数据库用 MySQL 8.0免费、广泛、默认字符集还支持 utf8mb4存蘑菇中文名和生僻描述都不会出现乱码。ORM 层我选了 MyBatis Plus而不是 Spring Data JPA。原因很简单这个项目的大部分操作是单表查询和多表关联查询MyBatis Plus 的BaseMapper直接提供了selectList、selectPage、selectById等常用方法省去写大量 XML。遇到蘑菇分类的递归查询或联表查询时再写自定义 SQL灵活性依然在。如果换 JPA单表 CRUD 确实也快但多表查询时要么写QueryJPQL要么用 Specification学习和排错成本都会高一些。这里多说一句MyBatis Plus 的QueryWrapper虽然好用但别在主业务查询里写得太复杂。举一个反面例子——如果你在 Service 层里嵌套三四个and()、or()一旦条件顺序写错排查 SQL 会让你崩溃。我的习惯是简单条件用LambdaQueryWrapper复杂搜索场景直接Service方法里拼 SQL宁可多写几行代码也要让生成的 SQL 在控制台一眼能看懂。项目里我加了一个MybatisPlusConfig只配置了分页插件没有其他花哨的东西——很多新手项目喜欢堆各种插件其实完全没有必要。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }2.2 前端技术栈Vue 2 还是 Vue 3前端的选型要结合团队情况和项目资料完整度来定。如果这是你的毕业设计或者个人作品我会推荐 Vue 2 Element UI 组合理由很直接网上的现成代码片段、踩坑帖子、经验文档最多遇到报错搜索解决方案时基本一找一个准。Vue 3 Vite Element Plus 当然更现代化但很多教学资料和开源项目还是 Vue 2 语法对新手来说Vue 2 上手成本明显更低。不过我在这个项目里用的是 Vue CLI 4 Vue 2.6因为团队成员之前写过 Vue 2 的后台管理系统对这个技术栈更熟。页面结构不复杂主要就是首页、列表页、详情页、后台管理页核心页面不到十个Vue 2 完全没有性能压力。组件库用 Element UI后台管理界面的表格、表单、弹窗、分页都能直接拿现成组件拼起来开发效率非常高。有一个细节值得强调前端项目要配置vue.config.js的 devServer 代理把/api前缀的请求代理到后端 8080 端口否则开发环境下就会遇到跨域问题。跨域问题虽然也可以通过后端加CrossOrigin解决但生产环境部署时前后端分离、域名不同跨域方案很容易变成隐患。最稳妥的做法是后端不写跨域代码统一走 nginx 反向代理或者开发环境用 devServer 代理。这个项目我最终选择的方案是 devServer 代理 生产环境 nginx 配置两套方式互不干扰。// vue.config.js module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } };2.3 整体架构中的一份标准数据流理清数据流这个事很多教程都不会重点讲但却是联调阶段最容易出问题的地方。以“用户搜索一种蘑菇”为例完整的数据流动过程是浏览器发起GET /api/mushroom/search?keyword香菇请求被 devServer 代理转发到后端MushroomController的方法Controller接收到参数后调用MushroomService的search方法Service通过 MyBatis Plus 的QueryWrapper构造模糊查询条件调用MushroomMapper执行 SQL数据库返回结果集Service将实体转成Result统一响应对象前端 Axios 在response拦截器里判断code字段如果等于 200则把data字段返回给页面渲染。这里最容易遗漏的一环是统一响应结构。很多新手写的接口每个方法返回的类型都不一样有的返回Map有的返回List有的直接返回实体导致前端 Axios 处理起来非常混乱。这个项目从一开始就约定了ResultT结构包含code、msg、data三个字段。所有 Controller 方法的返回值都是ResultT这样前端只需要写一次响应拦截逻辑后续接口全部复用开发效率提升非常明显。3. 数据库模型设计详解3.1 蘑菇信息表与关键字段说明数据库是这个项目的地基表设计不好后面写代码全是补丁。核心表是mushroom_info我把字段列出来并解释每个字段为什么存在字段名类型说明idbigint(20)主键自增namevarchar(50)正式中文名如“香菇”aliasvarchar(100)别名可存多个用顿号分隔sci_namevarchar(100)学名如“Lentinula edodes”category_idbigint(20)所属分类 ID关联分类表habitatvarchar(200)生长环境腐木、草丛、竹林等areavarchar(200)分布区域seasonvarchar(50)常见生长季节edibilitytinyint(1)可食性0 未知、1 可食、2 不可食、3 有毒descriptiontext形态特征详细描述image_urlvarchar(255)封面图 URLview_countint(11)浏览量用于排序create_timedatetime创建时间update_timedatetime更新时间edibility字段用整数而不是字符串这是一个有意的设计。如果直接用“可食”“有毒”这种中文存数据库查询时就得写中文条件容易在不同环境编码下出错而且以后想加一个“可食但需处理”的状态时改字符串很麻烦。用整数枚举代码里用一个EdibilityEnum做映射查询和扩展都方便。image_url默认存字符串这里有讲究。项目里图片上传后统一放在服务器/uploads/目录数据库只存相对路径/uploads/xxx.jpg。以后如果要迁移到 OSS 或改 CDN 地址只需要在 Nginx 或代码层统一加前缀不需要逐条改数据库。如果一上来就把http://localhost:8080/uploads/xxx.jpg存进数据库换服务器就得全量更新这是很常见的低级坑我见过不止一个项目吃过这个亏。3.2 分类表设计与递归层级分类表category的字段设计更有意思。为了支持“门/纲/目/科”这种多层级分类category表设计了id、name、parent_id、sort_order四个核心字段。parent_id为 0 表示顶级分类比如“担子菌门”父 ID 指向某条记录的id表示子分类比如“伞菌纲”的parent_id指向“担子菌门”这一行的 id。查询分类树时有两种方式一种是用 Java 递归拼装树结构适合数据量小的场景另一种是用 MySQL 8.0 的WITH RECURSIVE递归查询。我建议采用 Java 递归因为蘑菇分类的数据量撑死几百条递归查一次内存占用也微乎其微而且代码逻辑更透明、更容易调试。public ListCategoryVO buildTree(ListCategory allCategories, Long parentId) { ListCategoryVO tree new ArrayList(); for (Category category : allCategories) { if (parentId.equals(category.getParentId())) { CategoryVO node new CategoryVO(); BeanUtils.copyProperties(category, node); node.setChildren(buildTree(allCategories, category.getId())); tree.add(node); } } return tree; }这个递归方法有个小缺陷没有处理循环引用。如果分类数据录入时不小心把parent_id指成了自己就会无限递归直到栈溢出。后来我加了一层保护递归方法里传入已访问 ID 的 Set遇到重复就跳过虽然代码长了十行但稳定性好很多。这算是一个实用经验在常规教程里几乎不会被提到。3.3 用户、收藏与评论三张关联表用户表user字段不多id、username、password、nickname、avatar、role、create_time。密码字段存的是加盐后的 MD5 值不要存明文。MD5 本身不算安全但做一个演示项目够用了——如果你想更严谨一点换成 BCrypt 加密也就多几行配置的事。收藏表favorite是典型的关联表核心字段是user_id和mushroom_id再加一个唯一索引uk_user_mushroom(user_id, mushroom_id)防止同一用户重复收藏同一条蘑菇记录。前端“收藏”按钮的切换逻辑本质就是先查这张表里有没有对应记录有则删除没有则插入操作非常直观。评论表comment相对复杂一点id、mushroom_id、user_id、content、create_time、parent_id。parent_id的作用是支持回复楼中楼结构如果为空则表示对蘑菇的直接评论。这里用了逻辑删除标记deleted默认值为 0管理员删除评论时执行UPDATE comment SET deleted1而不是物理删除。为什么要这么做因为用户删除自己的评论后如果楼中楼里还有子评论物理删除会导致子评论变成孤儿数据页面渲染时找不到父级。逻辑删除则保留了整条评论链只是前端查询时统一过滤deleted0。数据库建表语句里面engine必须指定为 InnoDB字符集是utf8mb4_general_ci排序规则千万别图省事用utf8_general_ci——虽然大部分蘑菇描述用不上 emoji但万一哪天有生僻字utf8存不下就出大问题了。4. 后端接口与核心业务逻辑实现4.1 登录注册与 JWT 鉴权的小陷阱登录注册模块虽然几乎所有项目都有但写好并不容易。这个项目用的是 JWTJSON Web Token无状态鉴权方案流程是用户提交用户名密码后端验证通过后生成 token前端把 token 存到 localStorage每次请求在 header 里带Authorization: Bearer token后端通过拦截器解析 token获取当前用户信息。JWT 实现时的关键坑在于秘钥和过期时间。秘钥不能写死成secret这种弱口令要至少 32 位随机字符串过期时间我设的是 24 小时太长不安全太短又影响用户体验。另外后端拦截器要对/api/admin/**和/api/user/**做权限区分。管理员角色的 token 解析后role字段必须是admin才能访问管理接口。这里有一个非常常见的疏漏只拦截了“是否登录”没有拦截“是否有权限”结果随便注册一个普通用户就能调用管理员接口这就是越权漏洞。我的代码里用了一个自定义注解RequireAdmin加到管理员接口上拦截器统一校验比在每个 Controller 里手动判断优雅得多。if (adminPath.contains(requestURI)) { String role (String) claims.get(role); if (!admin.equals(role)) { writer.write(JSON.toJSONString(Result.error(403, 无权限访问))); return false; } }4.2 蘑菇列表分页与组合条件查询蘑菇列表页面承担了系统的主要流量所以后端查询接口的设计要兼顾灵活性和性能。接口路径设计为GET /api/mushroom/page?pageNum1pageSize10keyword香菇categoryId5edibility1。其中keyword是可选参数在 Service 层判断是否为空不为空则对name、alias、description三个字段做模糊匹配categoryId不为空则做精确匹配edibility同理。MyBatis Plus 的LambdaQueryWrapper写条件非常舒服几乎不用拼接 SQL 字符串LambdaQueryWrapperMushroomInfo wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.and(w - w .like(MushroomInfo::getName, keyword) .or() .like(MushroomInfo::getAlias, keyword) .or() .like(MushroomInfo::getDescription, keyword)); } if (categoryId ! null) { wrapper.eq(MushroomInfo::getCategoryId, categoryId); } if (edibility ! null) { wrapper.eq(MushroomInfo::getEdibility, edibility); } wrapper.orderByDesc(MushroomInfo::getViewCount); PageMushroomInfo page mushroomMapper.selectPage(new Page(pageNum, pageSize), wrapper);返回结果时直接返回Page对象前端可以从total字段拿到总记录数用于计算分页组件的总页数。搜索接口的性能瓶颈通常不在 SQL而在图片加载。列表页一次返回 10 条数据每条蘑菇的image_url可能达到几百 KB如果后端不加缩略图用户滑动页面时会非常卡。比较简单的解决办法是列表接口返回的图片 URL 统一拼上缩略图参数通过一个图片处理工具压缩到 300x300 分辨率。如果你用的是本地静态资源可以直接写一个ImageCacheController按需生成缩略图并缓存到磁盘。4.3 图片上传的完整链路与目录规划图片上传功能后端接口核心逻辑大概只有二十行代码但部署到服务器后坑非常多。开发环境里图片传上去显示正常一旦部署到 Linux 服务器路径问题就开始冒头。Windows 开发环境下File.separator是\Linux 是/如果代码里硬编码了\uploads\部署后就会 404。我的习惯是在配置文件中定义upload.path属性代码里通过ResourceUtils.getFile()解析绝对路径而不是直接用相对路径。上传接口的代码逻辑大致分四步接收MultipartFile校验文件类型和后缀名生成新文件名并保存到磁盘返回可访问的 URL。文件名用UUID.randomUUID()加原来的后缀避免重名覆盖。校验类型时只允许jpg、png、jpeg、webp这几种格式并且检查文件大小不超过 5MB——这个限制是在需求阶段和前端同学商量好的如果过大就提示压缩后再上传。String originalFilename file.getOriginalFilename(); int index originalFilename.lastIndexOf(.); String suffix originalFilename.substring(index); if (!allowedSuffixes.contains(suffix.toLowerCase())) { return Result.error(400, 不支持的图片格式); } String newFileName UUID.randomUUID().toString().replace(-, ) suffix; String dateDir new SimpleDateFormat(yyyyMMdd).format(new Date()); File dest new File(uploadPath File.separator dateDir, newFileName);图片路径按日期分目录存储是非常实用的小技巧。如果不分目录一年下来一个文件夹里会有几万张图片不仅打开时卡顿排查问题时也很痛苦。按yyyyMMdd分目录后每天一个文件夹结构清晰后续做定期清理或归档也方便。4.4 收藏与评论接口中的越权检查收藏和评论都属于用户相关内容接口设计时要特别注意越权问题。删除评论接口是/api/comment/delete/{id}普通用户只能删除自己发的评论。后端实现里的核心逻辑就是先按id查出评论再比较comment.getUserId()和当前登录用户的 id 是否一致不一致直接返回 403。管理员则可以删除任意评论。这个“先查后比”看似顺理成章但很多新手容易犯一个错直接按前端传来的参数删除完全没有校验归属。结果是任何一个登录用户只要抓包拿到别人的评论 id就能把别人的评论删掉这是典型的越权漏洞。面试时如果被问到“如何设计评论删除接口”把这一步的校验逻辑讲清楚是很好的加分项。5. Vue 前端页面实现细节5.1 路由组织与页面骨架设计前端路由的设计直接影响了项目的可维护性。这个项目分为两个布局区域前台和后台。前台是HomeLayout包含导航栏、首页、蘑菇列表、蘑菇详情后台是AdminLayout包含侧边栏、分类管理、蘑菇管理、用户管理。布局不同路由的组织方式也应该区分开{ path: /, component: HomeLayout, children: [ { path: , component: HomePage }, { path: mushroom/list, component: MushroomList }, { path: mushroom/detail/:id, component: MushroomDetail }, ] }, { path: /admin, component: AdminLayout, children: [ { path: category, component: CategoryManage }, { path: mushroom/edit, component: MushroomEdit }, { path: mushroom/edit/:id, component: MushroomEdit }, ] }路由组件懒加载值得用上。后台管理页面编译后的 JS 有几百 KB如果一次性全量加载首屏会明显变慢。用component: () import(/views/MushroomList.vue)按需加载首屏只加载必要的路由组件首页加载速度能提升 30% 左右。这些优化虽然简单但真实项目的体验差异非常大建议从一开始就养成懒加载的习惯。5.2 Axios 请求封装与拦截器配置前后端联调时最容易出现的问题就是接口路径不一致、状态码处理混乱。所以前端我统一封装了request.js里面做了两件重要的事统一注入 token统一处理响应异常。请求拦截器里从 localStorage 读取 token如果存在就加进请求头Authorization。响应拦截器里判断res.data.code不是 200 则提示错误信息是 401 则跳转到登录页。这样一个全局处理后面每个页面都不用重复写错误提示逻辑代码干净很多。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) { Message.error(res.msg || 请求失败); return Promise.reject(new Error(res.msg)); } return res; });有一个细节是后端返回 401 时Http 状态码不一定是 401。很多新手在后端拦截器里返回了Result.error(401)但 HTTP 状态码依然是 200前端在 Axios 响应拦截器里判断 HTTP 状态码导致 401 的 token 过期逻辑永远走不到。最稳妥的做法是前后端约定后端返回 401 时HTTP 状态码也设置为 401这样 Axios 的 error 分支就能正确捕获再在 error 分支里统一跳转登录页。这个约定一定要在接口文档里写清楚否则联调阶段的排查会浪费大量时间。5.3 蘑菇详情页与图片灯箱交互详情页是这个项目颜值最高的页面也是展示 Vue 组件通信能力的好场景。页面从上到下分别是封面图轮播、标题与基本信息标签区、形态特征区块、生长环境区块、用户收藏按钮、评论区。图片轮播可以直接用 Element UI 的el-carousel不过需要注意蘑菇详情页的图片可能只有一张el-carousel默认会循环播放一张图时箭头会很突兀。可以在前端判断图片数量只有一张时隐藏左右箭头。这种小细节很影响观感但很多教程不会讲。评论区值得多提一句。评论发布后前端需要重新拉取评论列表而不是手动拼接新评论。因为评论新增后要触发create_time排序重新计算、总数重新统计而且后端还可能有内容审核拦截手动插入新评论容易出现状态不一致。用reloadCommentList()无脑刷新虽然多一次网络请求但逻辑简单可靠。用户行为层的“收藏”按钮点击后直接调用/api/favorite/add或/api/favorite/cancel然后根据返回结果切换按钮状态。这里需要用v-if或动态 class 做视觉反馈但不建议在接口返回前就改 UI 状态否则接口失败时会出现按钮状态与数据不一致的假象。正确做法是等接口返回成功后再更新状态虽然可能慢几百毫秒但不会出现逻辑混乱。5.4 后台管理表单与富文本处理管理员编辑蘑菇信息时description字段内容较长我喜欢用el-input typetextarea加autosize属性来支持多行文本而不引入富文本编辑器。原因很简单富文本编辑器会生成带标签的 HTML如果后端没有做 XSS 过滤用户提交的script标签可能会执行造成安全风险。对于科普类网站纯文本描述已经够用没必要引入额外复杂度。表单提交时图片的上传是在子组件里完成的。用一个el-upload组件action指向/api/upload/image上传成功后把返回的 URL 写入父组件的表单数据对象中。这里有一个常见的坑el-upload默认用multipart/form-data提交后端接收时用RequestParam(file)参数名必须对齐。如果后端不是这个参数名文件会一直收不到报 400。我就在这个坑上浪费过半小时后来统一约定接口参数名为file并把校验逻辑写在接口文档第一行后面就没再出过问题。6. 项目启动与部署实录6.1 从零还原项目的环境准备如果你拿到了这个项目的源码包从前端到后端完整跑通一般需要下面几步。先说后端环境需要安装 JDK 1.8 或 11、Maven 3.6、MySQL 8.0。MySQL 里新建一个数据库执行项目根目录下的mushroom.sql脚本数据库名要和application.yml里的配置保持一致。spring: datasource: url: jdbc:mysql://localhost:3306/mushroom?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456后端启动前要检查两件事一是 MySQL 服务是否已启动二是application.yml里的端口是否被占用。项目默认端口 8080如果被占用改成 8081 或者 9090 都行但改完记得同步修改前端vue.config.js里的代理目标地址否则请求会全部 404。前端环境需要 Node.js 14 及以上版本。在项目目录里执行npm install安装依赖然后再执行npm run serve启动开发服务器。npm install如果报错多半是 npm 源的问题换成国内镜像源会快很多。6.2 前后端联调的三个常见问题联调阶段我踩得最多的坑有三个跨域、404、token 失效逐个说一下排查思路。跨域问题的典型报错是Access to XMLHttpRequest has been blocked by CORS policy。原因通常有几种前端代理没配好、后端接口路径错误、请求头缺少必要字段。排查顺序是先看 Network 面板里请求是否发到了后端地址再看代理是否正确转发最后看后端日志是否有请求进入。大部分跨域问题不是跨域本身而是代理没生效导致的。404 的问题集中在接口路径不一致。前端的/api/mushroom/page对应后端的RequestMapping(/api/mushroom)加GetMapping(/page)请求路径是/api/mushroom/page。如果前端多写了一个斜杠或者后端类上的路径写错就会出现 404。一个讨巧的方法是后端日志里会打印所有映射路径启动时扫一眼日志里的RequestMapping列表就能确认接口地址是否和前端一致。token 失效问题除了前端跳转逻辑没写好还有可能是后端拦截器对/api/upload路径也做了拦截。上传图片的请求需要携带 token但如果上传组件用的是el-upload的默认上传不会带上自定义 header就会因为 token 校验失败一直报 401。解决办法是为el-upload设置headers属性把 token 动态传入这样上传请求也能通过拦截器校验。6.3 生产环境部署与 Nginx 配置技巧开发环境跑通后部署上线还要处理静态资源、后端服务和数据库三者之间的关系。一种常见的简易部署方案是前端npm run build生成dist目录把dist和mushroom.jar放在同一台服务器上用 Nginx 同时托管前端页面和反向代理后端接口。server { listen 80; server_name your.domain.com; location / { root /app/mushroom-web/dist; index index.html; 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; } location /uploads/ { alias /app/mushroom-file/; } }Nginx 配置里有三个要点。第一try_files指令必须加否则 Vue Router 在 history 模式下刷新页面会出现 404。第二/api/的代理转发注意proxy_pass末尾的斜杠问题——proxy_pass http://127.0.0.1:8080;不带路径表示保持原请求路径转发即/api/mushroom/page转发到后端还是/api/mushroom/page。第三/uploads/的静态资源映射要和后端的upload.path配置对齐否则上传的图片无法通过 URL 访问。6.4 从零还原项目的常见问题速查表问题现象可能原因解决方案后端启动报数据库连接失败MySQL 未启动或密码错误检查 MySQL 服务核对application.yml配置前端npm run serve报端口占用8081 端口被其它进程占用修改vue.config.js中的端口请求后端接口返回 404接口路径不一致或代理未配置检查路由到后端的完整 URL核对代理 target上传图片返回 400文件参数名不是file检查el-upload的 name 属性改为file列表页图片加载缓慢原图过大未生成缩略图后端增加图片压缩逻辑或前端对图片进行懒加载刷新页面显示空白或 404未配置try_files在 Nginx 中加上try_files $uri $uri/ /index.html;用户评论删除提示权限不足删除接口未放行管理员后端接口增加RequireAdmin注解或角色判断7. 项目文档与二次开发建议源码包里附带了一份项目文档里面包含环境搭建说明、数据库变更记录、接口说明、部署步骤。我在实际使用中发现文档最有价值的部分不是环境搭建而是接口说明——里面记录了每个接口的请求参数、返回结构、注意事项。做二次开发时先翻接口文档再写代码比直接看源码更高效。如果接下来你自己想在这个项目上做扩展我建议优先做两个方向。第一是增加蘑菇识别功能调用图像分类接口前端上传蘑菇照片后端调用模型返回识别结果。这个功能非常契合“百科”场景也能让你的项目在同类作品中脱颖而出。第二是增加数据导出功能管理员可以把蘑菇列表导出成 Excel方便线下科普物料整理。技术上用EasyExcel实现后端一个接口加几十行代码就能搞定实用价值却很高。数据库层面如果你想扩展多图展示可以把image_url字段拆成mushroom_image子表支持一条蘑菇对应多张图片。这次改动涉及数据结构变化和详情页改造正好可以作为自己动手练手的好机会。8. 写在最后的经验复盘这个蘑菇百科项目做下来我最大的感悟是好项目不是“写”出来的而是“理”出来的。开始动手写代码之前把功能模块、数据表关系、接口路径、页面流转都梳理清楚后面真正开发的时间其实很短。如果在设计阶段一头雾水就直接开写大概率会在联调阶段陷入无尽的返工。另外想特别说一下后端返回结构统一这件事。很多教程项目不会强调ResultT统一返回结构但真实开发中这是前后端协作的基础约定。我在这个项目里将返回结构从一开始就定好后面所有接口都遵循这个格式前端写的拦截器才能统一处理。如果你正在看这个项目做练习可以先从这一步规范起养成好习惯。最后分享一个小技巧蘑菇百科的种子数据很重要。你拿到源码之后先把准备好的蘑菇数据导入数据库然后跑一遍前端页面把每种状态都点一遍比如搜索、分类、收藏、评论、后台编辑。只要种子数据够丰富这个项目跑起来的完整度和真实感就会很高无论是演示给面试官看还是用来自己学习联调效果都会好很多。项目本身不复杂但麻雀虽小五脏俱全认真走完一遍这套全栈流程对 Spring Boot 和 Vue 的理解会上一个明显的台阶。