Spring Boot+Vue旅游信息咨询网站全栈开发实战与部署指南
每年毕业季都会看到大量同学在选题表里反复筛选想要一个工作量饱满、技术面完整、答辩又可控的题目。“基于SpringbootVue的旅游信息咨询网站的设计与实现”这串名字几乎是各类选题列表里的常客属于典型的实用向全栈项目。它没有复杂的算法要求也没有高深的底层原理而是把前后端分离、数据库设计、接口规范、权限控制、部署上线这些日常开发中真正用得上的能力从头到尾完整走了一遍。这篇就围绕这个项目把技术选型、功能拆解、数据库建模、接口实现、前端联调、部署落地的全过程掰开揉碎讲清楚最后再分享一些实测踩坑经验。适合正在做课题设计、想系统提升全栈能力、或者打算拿一个完整项目练手的同学参考。1. 项目整体拆解这套系统到底在解决什么问题1.1 系统定位与使用场景旅游信息咨询网站核心业务一句话就能说清把游客感兴趣的信息——目的地景点介绍、路线行程、餐饮住宿、门票价格、出行攻略——通过一个统一入口展示出来让游客可以浏览、查询、收藏、评论同时让运营者通过后台维护这些内容。很简单对吧但这恰恰是这个项目的价值所在。它模拟的是真实业务系统中最常见的一种形态前台展示加后台管理。这种形态在任何行业都会出现旅游只是它的业务外壳。你把它替换成房产信息、招聘信息、活动报名、商品展示系统骨架几乎不用变化。做这个题目学到的东西完全可以迁移到其他项目里这也是它作为毕设选题长盛不衰的原因。从使用场景看系统需要覆盖两类完全不同的用户一类是普通游客也就是只浏览不管理的那批人。他们关心的是首页能不能快速看到热门景点和推荐路线景点详情是否包含图文介绍、门票价格、开放时间能不能搜索目的地能不能收藏喜欢的页面能不能发表评论这里有个容易被忽视的问题游客在使用过程中会留下收藏、评论、浏览记录等个人数据所以系统必须包含最基本的用户注册与登录这同时也为“带个人中心的数据访问控制”提供了天然的题目素材。另一类是后台管理员负责内容运营。他们要维护景点、路线的增删改查审核或者删除不当评论管理用户状态处理系统中的基础数据。管理员不需要关心前台页面长什么样但必须有一个清晰的后台界面能看到统计数据能高效地完成日常运维。1.2 功能需求梳理两条主线缺一不可把功能需求列清楚是所有后续工作的起点。这个项目我个人建议按照两条主线来梳理游客主线注册登录邮箱或手机号加密码注册登录后才可以收藏、评论首页展示轮播图、热门景点推荐、最新路线推荐景点模块景点列表、分页查询、关键词搜索、详情页展示图片/票价/开放时间/简介路线模块路线列表、路线详情包含每日行程、途经景点攻略资讯旅游攻略或新闻动态发布展示收藏功能收藏景点或路线个人中心查看评论功能对景点或攻略发表评论删除自己的评论管理员主线登录独立的后台入口可能复用用户表但增加角色字段数据看板总访问量、景点数量、用户数量、评论数量等基础统计景点管理CRUD支持图片上传路线管理CRUD维护路线与景点之间的关联攻略管理发布、编辑、下架用户管理查看用户列表、禁用/启用账号评论管理查看评论、删除违规内容把功能划分到这两条主线里后面的数据库设计、接口设计、页面规划才有章法。我自己的习惯是画一张简单的功能树不追求规范但一定要把所有功能点挂在树上至少挂到二级节点。画完之后表结构怎么建、接口要几个、页面要几块基本都能推导出来。2. 技术选型为什么是这套组合2.1 后端选型Spring Boot 的优势后端用 Spring Boot是这个项目最稳妥的选择。它最大的特点是开箱即用内嵌 Tomcat不需要额外部署容器打包成一个 jar 就能跑。对于课程设计和毕业设计来说这意味着评审老师要复现你的项目时只需要有 JDK 和 Maven不用关心一大堆环境配置。Spring Boot 还自带一套成熟的生态Spring MVC 负责接口和请求路由Spring Security 可以解决认证授权Spring Data JPA 或者 MyBatis 负责数据库访问。这些组件之间的整合成本极低很少需要手工写配置。很多人第一次接触 Spring Boot 时会把注意点放在“怎么把项目跑起来”上反而忽略了分层设计的价值。我建议在这个项目里严格做三层架构controller 层只负责接收参数、返回结果service 层放业务逻辑mapper 层配合 MyBatis处理数据库操作。举个例子删除一个景点不只是 delete 一条记录可能还要同时删掉景点关联的评论、收藏、路线关联记录这些业务规则就放在 service 里。如果全写在 controller 里代码会越来越乱接口越来越难维护。这个分层写清楚在答辩时是非常加分的点。2.2 前端选型Vue 2 还是 Vue 3前端使用 Vue需要在 Vue 2 还是 Vue 3 之间做一个选择。我的建议很简单如果模板、网上的现成代码、参考项目大多是 Vue 2 的写法那就优先 Vue 2如果打算从零开始并且愿意接受新生态可以选 Vue 3。Vue 的核心优势是组件化加数据驱动。页面被拆成组件组件维护自己的数据和状态数据变化时页面自动更新不需要手动操作 DOM。对于一个旅游信息网站来说景点列表、搜索框、分页组件、评论区这些天然适合组件化。Vue 配合 Element UI 或 Element Plus后台管理界面能很快成型。表格、表单、弹窗、菜单、标签页都可以直接用现成组件搭起来视觉上也统一比自己手写 CSS 省太多时间。前台页面如果想要更好的视觉效果可以用 Vite 或 Webpack 配合 SCSS自定义样式。如果用 Vue 3状态管理建议用 Pinia路由用 Vue Router 4UI 组件库用 Element Plus。如果用 Vue 2就对应 Vuex、Vue Router 3、Element UI。选型时踩坑最多的就是版本搭配我见过不少同学把 Vue 3 项目和 Element UI 装在一起报错报得怀疑人生。这里先记住一个结论Vue 3 配 Element PlusVue 2 配 Element UI不要混用。2.3 数据存储与访问层选择数据库这个项目用 MySQL 基本没有争议。MySQL 免费、普及率高、网上资料多适合大多数课题场景。如果对性能体量有更高要求或者想要云数据库托管也可以在后期把 MySQL 迁移到腾讯云、阿里云的数据库服务上但核心代码改动不大。数据库访问层我建议用 MyBatis 或者 MyBatis-Plus。MyBatis-Plus 尤其适合这种业务不复杂、但 CRUD 占大头的中型项目。它提供了通用 Mapper基础的增删改查不需要写 SQL单表操作直接调用方法就能完成这让代码量明显减少。多表关联查询比如路线关联景点也还是可以写自定义 SQL灵活性没有被剥夺。Hibernate / JPA 也是可选方案但对比下来国内参考项目、毕设模板、大多数线上教程都以 MyBatis 系列为主遇到问题时照着搜索能找到大量现成解决方案。做这类项目选“身边人最多”的技术永远比选“看起来最高级”的技术更实用。下面汇总一下技术栈的核心组合层次技术选择用途说明后端框架Spring Boot 2.7.xWeb服务与业务接口安全认证Spring Security JWT登录认证、接口权限控制ORM框架MyBatis-Plus数据库操作单表CRUD零SQL数据库MySQL 5.7 / 8.0存储业务数据前端框架Vue 2 Element UI 或 Vue 3 Element Plus构建前台与后台页面构建工具Maven / npm编译打包额外工具Redis可选缓存热点数据、存储验证码Redis 在毕设项目里不是必须的但如果你想把项目亮点做出来可以用 Redis 存首页热门景点的缓存或者存登录 token 的黑名单。这块可以作为扩展功能不属于基础要求。3. 数据库设计与核心表结构3.1 从需求到表的推导过程数据库设计是这个项目里最考基本功的环节也是最容易被答辩老师追问的地方。表设计合理不合理直接反映了你对业务的理解程度。根据前面梳理的功能需求我按实体划分出这几张核心表用户表存储注册用户和管理员景点表存储景点基础信息路线表存储旅游路线信息路线与景点关联表维护多对多关系攻略表存储旅游攻略、资讯文章收藏表记录用户收藏的景点或路线评论表记录用户评论内容轮播图表管理首页轮播图信息其中关系最核心的是景点和路线。一条路线包含多个景点一个景点也可能被多条路线覆盖这是典型的多对多关系。多对多在数据库里不能简单通过外键实现必须有一张关联表。我在这个项目里定义了一张 route_scenic 表字段就是 route_id 和 scenic_id再加一个自增主键。查询某条路线的所有景点直接 join 这张表即可。用户表和收藏、评论的关系相对简单都是“用户一多”加上“内容一多”的一对多关系。收藏表比较特殊它既可能指向景点也可能指向路线这里有两个设计方案方案一在收藏表里增加 target_type 字段表示收藏的是景点还是路线对应 target_id 存具体业务对象的 id。 方案二拆成两张表favorite_scenic 和 favorite_route。方案一更通用表数量更少查询时只需要根据类型拼接不同的关联表代码里用一个 switch 或 if 分支解决。方案二更直观不用区分类型但如果后续新增攻略收藏就得加新表。我采用的是方案一推荐让项目保持简洁同时也体现一点点抽象设计能力答辩时能讲出设计理由。评论表也是同样的做法评论一个景点或一个攻略通过 type 字段区分评论对象。在设计这些表时有一个容易踩的坑把所有字段都放在一张大表里想着后面要什么就加什么字段。这种做法看起来方便实际上会让表结构越来越乱字段语义不清查询时也要反复判断。例如把收藏表和评论表合并就会出现很多空字段既浪费存储又增加代码复杂度。宁可多建一张表也不要不合理地合并字段。3.2 核心表的字段设计以景点表为例字段设计建议如下字段名类型说明idbigint主键自增namevarchar(100)景点名称cover_imagevarchar(255)封面图URLimagestext多图URLJSON字符串存储addressvarchar(255)景点地址pricedecimal(10,2)门票价格open_timevarchar(100)开放时间descriptiontext景点简介hit_countint浏览量statustinyint状态1上架0下架create_timedatetime创建时间这里有几个设计点值得展开。第一个是 images 字段用 text 类型存 JSON 字符串。很多人在设计图片字段时只设计一个 image 字段但实际景点详情页往往需要多张图。多张图的存储方式有几种建一张景点图片子表、用逗号分隔URL、用 JSON 数组。子表最规范但操作起来要额外维护一张表JSON 数组最灵活查询时在 Java 里直接转 List 就行。对这个项目的规模JSON 数组更实用。第二个是 price 用 decimal 类型。景点门票是金额float、double 在做浮点运算时会损失精度比如 0.1 0.2 不等于 0.3。钱的字段必须用 decimal这个细节如果被答辩老师点到答不上来会显得很基础不扎实。第三个是 status 字段。业务上删除景点通常不是物理删除而是逻辑删除或上下架。把 status 设为 0前台就看不到这个景点但后台仍然可以恢复。这比直接 delete 更安全也更符合实际业务的运营习惯。MyBatis-Plus 还提供了逻辑删除注解一条配置就能实现这个逻辑。用户表的设计在这里也值得单独提一下。除基础字段外role 字段可以区分用户和管理员角色0 为普通用户1 为管理员。password 字段存储加密后的密码建议使用 BCrypt 算法加密而不是明文。security 相关的内容网上资料很多但真正把密码加密做好的人不多。我用一个简单例子说明用户在注册时提交的 password 是“123456”如果直接存数据库一旦数据库泄露所有用户密码都会直接暴露。BCrypt 会把它变成一串长度 60 左右的随机字符串并且每次加密结果都不一样破解成本非常高。3.3 初始数据准备数据库设计完成后落地时还要准备一批初始数据。这看似不起眼其实直接影响项目展示效果。一个空荡荡的旅游网站首页没有景点、没有路线、没有攻略评审老师打开第一眼印象就打折扣。我建议初始数据至少准备10 个左右景点内容覆盖不同地区和类型带图片 URL3 到 5 条路线每条路线关联 3 到 5 个景点5 篇攻略文章一个管理员账号和一个普通测试账号若干条模拟评论。所有图片 URL 可以先用网上公开的占位图片链接也可以把几张测试图片放在项目的 static 目录下避免依赖外部网络。初期我也有一次因为图省事只准备了两条数据结果前端分页组件、搜索功能都要等翻到第二页才能看到效果自己调试时效率很低。充足的初始数据是所有功能验证的基础这一步值得花时间认真准备。4. 后端接口实现与核心逻辑4.1 分层结构与统一返回格式后端代码的分层我强烈建议从一开始就按统一规范来。包结构大致是com.example.travel ├── controller接口层 ├── service业务层 ├── mapper数据访问层 ├── entity实体类 ├── config配置类 ├── security认证授权相关 ├── dto前端传参接收对象 └── vo返回前端数据对象entity 对应数据库表结构dto 和 vo 需要和二三层区分清楚。很多初学者在写接口时接收前端参数直接用 entity返回数据也直接用 entity这样做小项目勉强可用但问题在于数据库表结构一旦调整接口就会跟着变动此外有些字段你并不想暴露给前端比如用户的 password如果直接返回 entity密码就直接泄露到浏览器网络面板里了。一个相对规范的做法是接收参数用 dto返回数据用 vo。比如分页查询景点前端传来 pageNum、pageSize、keyword后端把它们封装到 PageQueryDTO查询结果返回一个 PageResultVO里面包含 total 和 records 字段。这样层次清晰也能防止把不必要的字段传出去。统一返回格式也很重要。我在项目中定义了一个通用的 RespResult 类包含 code、message、data 三个字段。所有接口都返回这个结构。前端拿到数据后先判断 code 是否为 200再取 data 使用。这样做的好处是前后端处理逻辑高度统一错误情况也能通过 message 字段准确定位到原因。4.2 基于 JWT 的认证流程认证模块是整个系统的核心基础设施。这里用 Spring Security 加 JWT 的方式整体流程如下用户登录时后端校验用户名密码校验通过后生成一个 JWT token返回给前端。前端把 token 存储到 localStorage 或者 Vuex/Pinia 里。后续每次请求前端在请求头里带上 Authorization: Bearer 加 token 的字段。后端通过过滤器解析 token获取当前用户 id 和角色设置到上下文中。这个方案不需要在服务端保存会话状态天然适用于前后端分离架构。写代码时核心包括三部分JwtTokenUtil 负责 token 的生成与解析JwtAuthenticationFilter 负责拦截请求、解析 token、把用户信息放入 SecurityContextSecurityConfig 负责配置哪些路径匿名可访问哪些路径需要登录。配置路径权限时有一个经验登录接口、注册接口、首页展示接口、景点列表接口、景点详情接口属于公开接口游客不需要登录也能访问。而收藏、评论、个人中心相关的接口必须有登录权限。管理员接口必须同时校验管理员角色。刚开始实现时最常见的错误是过滤器里忘记处理 token 无效的情况导致接口直接报 500。正确的做法是token 无效或过期时过滤器放行请求但不设置已登录的上下文Spring Security 后续在访问受保护接口时看到上下文里没有认证信息会统一返回 401。这样错误不会在过滤器里爆炸而是由框架统一处理。4.3 核心业务接口的实现经验景点的分页搜索接口是项目里最常用的接口逻辑不复杂但实现时要注意查询条件的组合方式。MyBatis-Plus 提供一个 QueryWrapper可以动态拼接条件public PageResultVOScenicVO pageScenics(ScenicPageQuery query) { PageScenic pageParam new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperScenic wrapper new LambdaQueryWrapper(); // 关键词匹配景点名称 if (StringUtils.hasText(query.getKeyword())) { wrapper.like(Scenic::getName, query.getKeyword()); } // 价格区间筛选 if (query.getMinPrice() ! null) { wrapper.ge(Scenic::getPrice, query.getMinPrice()); } if (query.getMaxPrice() ! null) { wrapper.le(Scenic::getPrice, query.getMaxPrice()); } // 只查询上架的景点 wrapper.eq(Scenic::getStatus, 1); wrapper.orderByDesc(Scenic::getHitCount); PageScenic result scenicMapper.selectPage(pageParam, wrapper); return convertToPageVO(result); }这类代码写得多了以后会发现几乎所有列表查询都是同一个套路分页、动态条件、排序。把这个套路掌握住一个项目里的列表接口基本都能快速写出来。评论接口的设计需要特别注意事务。用户删除评论这个操作如果评论被其他表引用了比如统计评论数为 0 时展示“暂无评论”这里要保证删除之后景点或攻略的评论数同步更新。推荐在删除评论的 service 方法上加上 Transactional 注解。这个注解保证了如果后续任一 SQL 语句抛出异常整个操作方法内的所有数据库操作都会回滚不会出现评论删掉了但评论数还是原来那个值的脏数据情况。收藏功能的实现要考虑一个简单的幂等问题同一个用户收藏同一个景点第二点击应该是取消收藏而不是重复收藏。所以收藏接口的写入逻辑要先把旧数据查出来Favorite favorite favoriteMapper.selectOne( new LambdaQueryWrapperFavorite() .eq(Favorite::getUserId, userId) .eq(Favorite::getTargetId, targetId) .eq(Favorite::getTargetType, targetType) ); if (favorite ! null) { // 已收藏取消收藏 favoriteMapper.deleteById(favorite.getId()); } else { // 未收藏新增收藏 Favorite newFavorite new Favorite(); ... favoriteMapper.insert(newFavorite); }这样可以保证一个用户对一个对象最多只有一条收藏记录。如果用户在收藏接口上做并发点击极端情况下还可能出现重复记录解决思路是给字段加唯一索引比如用户 id、目标 id、目标类型组成联合唯一索引。用数据库层约束保证不会重复代码层再做一个前置判断双层保险。4.4 图片上传与静态资源映射景点、路线、攻略都需要图片。图片上传有两种常见方式一种是传到七牛云、阿里云 OSS适合公网正式部署另一种是上传到本地服务器指定目录访问时通过 Spring Boot 的静态资源映射来暴露。毕设项目大多数用的是第二种方式部署简单且不用额外购买云服务。本地存储的实现思路前台上传文件后端接收 MultipartFile 后用 UUID 生成新文件名然后把文件写入到服务器磁盘目录比如 resource/upload/。数据库里存的是图片访问 URL比如 /upload/xxxx.jpg。最后在 WebMvcConfig 里配置静态资源映射Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry .addResourceHandler(/upload/**) .addResourceHandler(file: uploadDir); }把 uploadDir 配置到 application.yml 里这样以后部署到新环境只要改配置文件里的路径即可代码不用动。关于文件大小建议在配置里限制最大上传体积比如 10MB避免有人上传大文件把服务器磁盘占满。同时要记住上传接口必须做登录校验不能放匿名访问。5. 前端 Vue 的实现与前后端联调5.1 路由规划与页面划分前端部分项目以 Vue 为例整个页面可以拆成三大块游客端前台页面首页景点列表页景点详情页路线列表页路线详情页攻略列表页与详情页登录注册页用户个人中心子页面个人资料我的收藏我的评论管理员后台后台登录可以复用登录页登录后根据角色跳转数据看板景点管理路线管理攻略管理用户管理评论管理路由规划时要考虑嵌套路由结构。比如个人中心是一个一级路由组件里面再用 children 配置二级路由对应各个子页面。后台管理同理侧边栏菜单切换时只是中间内容区域发生变化外层布局组件保持不动。Vue Router 的嵌套路由天然适合这种布局实现起来也比较顺手。5.2 Axios 请求封装与拦截器调试联调阶段最值得做的一件事是把 axios 的请求和响应拦截器配好。请求拦截器里读取本地存储里的 token放到 headers响应拦截器里统一处理 code 不等于 200 的情况并提示用户重新登录或跳转登录页。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) { ElMessage.error(res.message || 请求失败) if (res.code 401) { router.push(/login) } return Promise.reject(new Error(res.message)) } return res.data }, error { ElMessage.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } )有了这层封装业务页面里调用接口只需写一行代码不需要关心 token 和错误处理。这份封装的代码在后来做其他项目时也能直接复用属于一次投入长期受益。5.3 状态管理当前用户信息与购物车式逻辑用户登录成功后后端返回 token 和用户基本信息。token 存到 localStorage 是为了刷新页面后不丢失登录状态。用户的基本信息我建议再存一份到 Pinia 或 Vuex 的 store 里方便全局使用。比如导航栏展示用户名、头像需要在组件里从 store 读取而不是每个页面都调用一次接口查询用户信息。还需要考虑刷新页面后如何恢复用户状态。最简单的方法是登录时把用户基本信息 JSON 序列化存到 localStorage刷新页面后从 localStorage 读取并填充到 store。稍微严谨一点的做法是每次刷新后调用后端接口获取当前登录用户信息。第二种方案后端需要增加一个获取当前用户信息的接口实现不复杂却能保证信息是最新的权限也更安全。我倾向于第二种方案对答辩来说也多一个可以讲的技术点。5.4 联调经验跨域与代理前端开发服务跑在 8080 端口后端接口跑在 8081 端口浏览器里就会遇到跨域问题。解决跨域的方式有几种后端配置跨域注解前端配置 proxy 代理生产环境用 Nginx 反向代理。开发阶段最推荐的做法是配置 Vite Dev Server 或 Webpack Dev Server 的 proxy 代理前端请求 /api 开头的地址时自动代理到后端的 http://localhost:8081 上去。这样浏览器看到的请求是同源的规避了跨域问题而且代码里直接写 /api 路径不写完整的域名端口部署时更换后端地址也很方便。第一种后端配置跨域注解的方式开发时用起来方便但线上如果部署在同一个域名下其实没有必要配置还可能带来安全风险。所以在开发阶段我还是建议前端用代理后端不用额外配置跨域。联调阶段最容易出现的错误是前后端字段名不一致。比如数据库表字段叫 nameJava 实体里是 namejson 序列化后还是 name结果前端代码里写成了 scenicName自然取不到数据。解决这类问题需要后端统一使用小驼峰命名前端拿到接口返回的数据后先用浏览器控制台 console.log 查看实际字段名再写页面逻辑不要靠猜。6. 部署流程与典型问题排查6.1 从开发到部署的完整流程项目验收和部署上线是两件不同的事。验收时可能只要本地能把前后端跑起来但部署文档里通常要求描述如何在服务器上运行。最稳妥的基础部署方式是后端打包成 jar前端构建成静态资源再用 Nginx 做反向代理。后端打包时注意 application.yml 里的数据库连接配置。本地开发是 localhost服务器上是云数据库或服务器本地 MySQL 地址。建议在配置文件里把数据库密码等私有信息抽到环境变量或外部配置打包时不用改代码部署时设置环境变量即可。直接用 mvn clean package 打包目标目录下生成的 travel-0.0.1-SNAPSHOT.jar 就是可以运行的文件。前端打包在项目根目录执行 npm run build产物在 dist 目录。把 dist 目录里所有文件上传到服务器的指定目录比如 /var/www/travel/。然后配置 Nginxserver { listen 80; server_name yourdomain.com; root /var/www/travel; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }location /upload/ { alias /data/travel/upload/; }}这段配置里最关键的是 frontend 路由的 history 模式。Vue Router 使用 history 模式时刷新 /scenic/1 页面如果 Nginx 没有 try_files 的 fallback就会返回 404。try_files $uri $uri/ /index.html; 就是解决这个问题的所有找不到的路径都回退到 index.html由前端路由接管。这一点在部署文档里一定要写清楚否则刚部署完刷新页面就白屏是特别典型的部署坑。 ### 6.2 后端常见问题与排查思路 问题一启动时报数据库连接失败。 原因一般是 MySQL 没启动或者配置里的用户名密码不对。排查时先确认 MySQL 服务状态再检查连接地址。还有一个常见坑是 MySQL 8.0 的驱动类名与 5.7 不同必须保持一致。 问题二接口返回的时间格式不对比如显示为时间戳。 这是 Java 时间序列化的问题。在 application.yml 里配置时间格式即可 yaml spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8问题三跨域配置失效或重复。 如果前后端都配置了跨域会出现 Access-Control-Allow-Origin 重复的报错。建议后端不做跨域配置完全交给前端代理或 Nginx。把跨域逻辑收敛到一层排查时也简单。问题四MyBatis-Plus 的分页不生效。 使用分页插件时必须配置分页拦截器。在项目中注入 P 的大写 PaginationInnerInterceptor 到 MybatisPlusInterceptor并设置数据库类型为 MySQL否则分页查询返回的数据不带 total 和正确分页效果。6.3 前端常见问题与排查思路问题一npm install 报错依赖版本冲突。 优先锁版本不要使用最新版。使用 Vue 2 就锁 Vue 2.6 或 2.7 版本Element UI 不要安装到 2.15.3 以下版本。npm install 报错时查看报错信息里提示的 unrecognized peer dependencies逐条调整版本。问题二刷新后页面白屏。 优先检查路由配置的 history 模式与后端静态资源是否配置了 try_files。如果是在本地开发环境有浏览器直接访问非根路径时开发服务器也存在 history fallback 配置Vite 默认会处理但 Webpack 需要手动配置 historyApiFallback。这个问题多见于部署环境。问题三表格数据不显示但接口调用是正常的。 检查前面提到的响应拦截器返回的数据格式。如果拦截器返回了 res.data.data页面里又调了一次 res.data就会拿不到数据。记住响应拦截器的返回层级页面上只管拿数据不要再多层嵌套去点。6.4 答辩演示与交付材料的准备这个项目的交付材料通常包括源码、lw论文文档、部署文档、讲解视频。源码就不多说了重点聊一下部署文档和演示准备。部署文档建议写得像一份操作手册环境准备清单、数据库初始化步骤、后端 jar 启动步骤、前端静态文件放置说明、Nginx 配置内容、访问地址和默认账号。每一步都配上操作命令和预期输出。这份文档不光是给评审老师看的也是给两三个月以后的自己看的。当时写项目花了一个月如果中间隔了两个月再重新部署没有这份文档的帮助真的很难快速恢复现场效率很低。答辩演示前必须预演一遍核心链路注册一个新用户登录浏览景点搜索收藏一个景点发表评论切换到管理员账号在后台看到新增评论并删除。这个链路覆盖了前后端联动、数据库写入、权限切换等核心能力演示顺畅基本能向评审老师证明你完整掌握了整个项目。我个人第三次答辩模拟时发现一紧张就容易忘记先演示管理员功能导致后面时间不够。后来我准备了一份演示序列表把功能节点、操作路径、注意事项都列出来这样不管临场状态如何都不会乱。这套方法在很多项目演示中都适用建议复制这套方法论。最后再说一个容易被忽略的细节项目里的硬编码信息比如数据库密码、第三方接口地址、服务器 IP尽量不要直接写在源码里提交到代码库。至少把它们放到配置文件并加入忽略清单。这样做既是对安全习惯的养成也能避免代码在评审过程里被问到敏感信息时陷入尴尬。把自己的代码当成交付给对方的产品来对待这个态度本身就是项目之外最值得收获的东西。