每年毕业季,都会看到大量同学在选题表里反复筛选,想要一个工作量饱满、技术面完整、答辩又可控的题目。“基于Springboot+Vue的旅游信息咨询网站的设计与实现”这串名字,几乎是各类选题列表里的常客,属于典型的实用向全栈项目。它没有复杂的算法要求,也没有高深的底层原理,而是把前后端分离、数据库设计、接口规范、权限控制、部署上线这些日常开发中真正用得上的能力,从头到尾完整走了一遍。这篇就围绕这个项目,把技术选型、功能拆解、数据库建模、接口实现、前端联调、部署落地的全过程掰开揉碎讲清楚,最后再分享一些实测踩坑经验。适合正在做课题设计、想系统提升全栈能力、或者打算拿一个完整项目练手的同学参考。
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 4,UI 组件库用 Element Plus。如果用 Vue 2,就对应 Vuex、Vue Router 3、Element UI。选型时踩坑最多的就是版本搭配,我见过不少同学把 Vue 3 项目和 Element UI 装在一起,报错报得怀疑人生。这里先记住一个结论:Vue 3 配 Element Plus,Vue 2 配 Element UI,不要混用。
2.3 数据存储与访问层选择
数据库这个项目用 MySQL 基本没有争议。MySQL 免费、普及率高、网上资料多,适合大多数课题场景。如果对性能体量有更高要求,或者想要云数据库托管,也可以在后期把 MySQL 迁移到腾讯云、阿里云的数据库服务上,但核心代码改动不大。
数据库访问层,我建议用 MyBatis 或者 MyBatis-Plus。MyBatis-Plus 尤其适合这种业务不复杂、但 CRUD 占大头的中型项目。它提供了通用 Mapper,基础的增删改查不需要写 SQL,单表操作直接调用方法就能完成,这让代码量明显减少。多表关联查询(比如路线关联景点)也还是可以写自定义 SQL,灵活性没有被剥夺。
Hibernate / JPA 也是可选方案,但对比下来,国内参考项目、毕设模板、大多数线上教程都以 MyBatis 系列为主,遇到问题时照着搜索,能找到大量现成解决方案。做这类项目,选“身边人最多”的技术永远比选“看起来最高级”的技术更实用。
下面汇总一下技术栈的核心组合:
| 层次 | 技术选择 | 用途说明 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.x | Web服务与业务接口 |
| 安全认证 | 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 核心表的字段设计
以景点表为例,字段设计建议如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| name | varchar(100) | 景点名称 |
| cover_image | varchar(255) | 封面图URL |
| images | text | 多图URL,JSON字符串存储 |
| address | varchar(255) | 景点地址 |
| price | decimal(10,2) | 门票价格 |
| open_time | varchar(100) | 开放时间 |
| description | text | 景点简介 |
| hit_count | int | 浏览量 |
| status | tinyint | 状态:1上架,0下架 |
| create_time | datetime | 创建时间 |
这里有几个设计点值得展开。
第一个是 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 个左右景点,内容覆盖不同地区和类型,带图片 URL;3 到 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、把用户信息放入 SecurityContext;SecurityConfig 负责配置哪些路径匿名可访问,哪些路径需要登录。
配置路径权限时有一个经验:登录接口、注册接口、首页展示接口、景点列表接口、景点详情接口属于公开接口,游客不需要登录也能访问。而收藏、评论、个人中心相关的接口必须有登录权限。管理员接口必须同时校验管理员角色。
刚开始实现时最常见的错误是过滤器里忘记处理 token 无效的情况,导致接口直接报 500。正确的做法是:token 无效或过期时,过滤器放行请求但不设置已登录的上下文,Spring Security 后续在访问受保护接口时看到上下文里没有认证信息,会统一返回 401。这样错误不会在过滤器里爆炸,而是由框架统一处理。
4.3 核心业务接口的实现经验
景点的分页搜索接口是项目里最常用的接口,逻辑不复杂,但实现时要注意查询条件的组合方式。MyBatis-Plus 提供一个 QueryWrapper,可以动态拼接条件:
public PageResultVO<ScenicVO> pageScenics(ScenicPageQuery query) { Page<Scenic> pageParam = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<Scenic> 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); Page<Scenic> result = scenicMapper.selectPage(pageParam, wrapper); return convertToPageVO(result); }这类代码写得多了以后会发现,几乎所有列表查询都是同一个套路:分页、动态条件、排序。把这个套路掌握住,一个项目里的列表接口基本都能快速写出来。
评论接口的设计需要特别注意事务。用户删除评论这个操作,如果评论被其他表引用了,比如统计评论数为 0 时展示“暂无评论”,这里要保证删除之后,景点或攻略的评论数同步更新。推荐在删除评论的 service 方法上加上 @Transactional 注解。这个注解保证了如果后续任一 SQL 语句抛出异常,整个操作方法内的所有数据库操作都会回滚,不会出现评论删掉了但评论数还是原来那个值的脏数据情况。
收藏功能的实现,要考虑一个简单的幂等问题:同一个用户收藏同一个景点,第二点击应该是取消收藏,而不是重复收藏。所以收藏接口的写入逻辑要先把旧数据查出来:
Favorite favorite = favoriteMapper.selectOne( new LambdaQueryWrapper<Favorite>() .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 路径,不写完整的域名端口,部署时更换后端地址也很方便。
第一种后端配置跨域注解的方式,开发时用起来方便,但线上如果部署在同一个域名下其实没有必要配置,还可能带来安全风险。所以在开发阶段我还是建议前端用代理,后端不用额外配置跨域。
联调阶段最容易出现的错误,是前后端字段名不一致。比如数据库表字段叫 name,Java 实体里是 name,json 序列化后还是 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/。然后配置 Nginx:
server { 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: GMT+8问题三:跨域配置失效或重复。 如果前后端都配置了跨域,会出现 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,尽量不要直接写在源码里提交到代码库。至少把它们放到配置文件并加入忽略清单。这样做既是对安全习惯的养成,也能避免代码在评审过程里被问到敏感信息时陷入尴尬。把自己的代码当成交付给对方的产品来对待,这个态度本身,就是项目之外最值得收获的东西。