做了几年 Java 后端,也带过不少刚入行的同事,我发现一个特别典型的现象:很多人会写单接口、会搭单个页面的 Demo,但一碰到“完整系统”这种要求,脑子里立刻一团浆糊——表怎么建、模块怎么拆、前后端怎么接、环境怎么配,每一步都在踩坑。今天想拿这个“基于 SpringBoot + Vue 的安康旅游网站管理系统”做例子,完整拆一遍从零到部署上线的全过程,项目不大不小,刚好覆盖全栈开发里最核心的技术点,非常适合拿来练手、做课设,或者作为简历上的实战项目。
整个系统的核心链路不复杂:前端用 Vue 做单页面应用,后端用 SpringBoot 提供 RESTful API,MySQL 存业务数据,MyBatis 负责数据库访问层,最终打成 jar 包和静态资源包,部署到一台服务器上就能跑。这套技术栈是现在中小型 Web 项目里最主流的搭配之一,学会了它,你再看别的 Java Web 项目会轻松很多。下面我按自己实际开发的顺序来写,尽量把每一步背后“为什么这么做”也讲清楚。
1. 从选题到落地:旅游网站系统的需求边界与功能地图
很多新手拿到“旅游网站管理系统”这种题目,第一反应就是把旅游网站往大了做——要在线支付、要地图导航、要智能推荐,结果被复杂度活埋。我建议立项时先把边界划清楚,一个管理系统最核心的价值就两个方向:对外展示和信息管理。安康旅游网站管理系统也是如此,拆开来看其实是两个端。
1.1 前台展示端:游客能看到什么
前端对应的是游客访问的网站页面,也就是一个旅游门户。游客进来之后,重点体验路径是:浏览景点列表 → 查看景点详情 → 搜索和筛选自己感兴趣的景点 → 顺手看看相关酒店、旅游线路和攻略文章。
所以在设计页面时,我划分了这几个核心页面模块:
- 首页:展示 banner、热门景点推荐、最新攻略等聚合信息,是整个门户的信息门面
- 景点列表/详情:按城市、区域、分类筛选景点,详情页展示景点图片、简介、开放时间、门票价格等
- 酒店频道:酒店列表、房型介绍,支持按价格、星级排序
- 线路频道:展示一日游、多日游线路,线路详情里挂关联景点
- 攻略文章:图文形式的游玩攻略,按时间倒序排列
- 用户登录/注册:前台用户注册登录之后才能发表评论、收藏景点
前台的重心是“浏览体验”,所以数据查询要快,页面要结构清晰,尤其是景点和线路的关联数据,要设计好查询口径。
1.2 后台管理端:管理员怎么维护数据
后台是给系统管理员用的,功能上有明确的 CRUD 特征,这也是毕设和课设考察的核心。后台包含这些模块:
- 管理员登录与权限控制(基于 Session 或 Token 都可以,本项目可以先用拦截器做登录校验)
- 景点管理:对景点数据做增删改查、上架/下架,支持图片上传
- 酒店管理:维护酒店和房型数据,操作逻辑和景点类似
- 线路管理:维护线路基本信息,并关联多个景点
- 攻略管理:发布、编辑、删除攻略文章
- 用户管理:查看注册用户列表,禁用/启用用户
- 评论管理:查看、删除用户提交的评论
- 数据统计(可选):按月份统计景点访问量之类的基础图表
旅游系统说到底就是内容管理系统的一个行业变种,后台就是在做模型的标准操作。把后台做扎实了,项目的技术说服力就有了。
1.3 用例视角下的角色与权限
从系统角色上看,整个系统只涉两类角色:管理员和普通用户。管理员拥有后台全部权限;普通用户只能在前台浏览,登录后可评论、收藏。这样设计的好处是权限模型极其清晰,适合用小规模的拦截器和 Session/Redis 做控制,而不必引入 Spring Security 这种体量的框架。
如果后续想往简历上写亮点,可以把权限这块升级成基于角色的访问控制模型,加入 JWT 认证、Redis 存储 Token,那就完全是另一档次的复杂度了。但刚开始,千万别一上来就上全家桶,先跑通主链路最重要。
2. 技术选型的账要算清楚:SpringBoot + Vue + MySQL + MyBatis 是不是最优解
这套技术栈经常被人说是“毕设标配”,话里带点嫌弃,但真拿去做中小型系统,它依然是性价比极高的组合。我从业多年,见过不少项目用更“高大上”的框架做得一塌糊涂,反而这种主流稳妥的组合更容易出活、更容易维护。下面把四个核心技术的分工和选型理由逐个讲清楚。
2.1 SpringBoot:为什么用它而不是传统 Spring
SpringBoot 最大的价值不是新发明了什么,而是把 Spring 的配置复杂度给收编了。传统 Spring 项目要写大量 XML 配置——数据源、事务、组件扫描、视图解析器,每一样都要手动声明,对新手很不友好。SpringBoot 用“约定大于配置”和“自动配置”这两招,把常见场景的默认配置全部提前打好包,你只需要在 application.yml 里写上少量个性化配置就能启动。
用个生活化的比喻:Spring 是买了毛坯房,所有硬装软装都要自己搞;SpringBoot 是精装房,水电管线全部预埋好了,你搬进来只需要买点家具家电就能住。对于业务系统开发,我们需要的是快速把业务跑起来,而不是花时间调框架配置。
另外一个特别实用的点是 SpringBoot 内嵌了 Tomcat 服务器,打包成可执行的 jar 文件后,服务器上只需要装 JDK 就能跑,这对部署来说简直是降维打击。传统 Spring 项目打包成 war 还要装 Tomcat、还要配 context path,麻烦得多。
2.2 Vue:前后端分离下的页面构建方案
选择 Vue 不止是因为它热度和文档友好,而是它的渐进式架构非常适合这个项目。Vue 的模板语法上手快,v-model 双向绑定能轻松处理表单数据流,组件化开发可以把头部导航、景点卡片、轮播图这些重复出现的 UI 块抽成独立组件复用。
前后端分离模式下,Vue 项目本身是一个独立的工程,通过 axios 发起 HTTP 请求调用后端的 JSON API,和后端完全解耦。开发时可用 Vite 或 Vue CLI 起一个本地开发服务器,接口代理到后端端口,生产环境则由 Nginx 来接管静态文件和反向代理。这种架构带来的直接好处是,后端接口只要写好文档,前端可以并行开发,不会互相卡住。
如果你对这块还不熟悉,可以先用 Vue 2 + Vue CLI 快速搭项目,很多公开的教程和插件都基于这个组合,碰到问题也容易搜索到解决方案。使用 Vue 3 + Vite 也没有问题,本质开发路径是一致的。
2.3 MySQL + MyBatis:数据持久层的最稳妥搭档
MySQL 的量级和复杂度正好匹配这种旅游网站——单机部署、数据量几万到几十万级别、需要事务支持、需要灵活的查询。景区、酒店、用户、评论这些数据之间天然存在外键关联,用关系型数据库存储是直觉正确的选择。
MyBatis 的核心优势是“SQL 在你的掌控之中”。与 JPA/Hibernate 这种全自动 ORM 相比,MyBatis 不会替你生成神奇的全表查询,也不会在复杂关联查询上拐弯抹角。你可以手写 SQL、理解 SQL,这对于学习数据库原理很有帮助,也方便后期针对慢查询做优化。另外,MyBatis 在处理动态 SQL 时提供了 where、if、foreach 等标签,酒店条件筛选这种多条件组合的查询写起来非常舒服。
用 MyBatis 的时候我建议配合代码生成插件(如 MyBatis Generator 或 MyBatis-Plus,看团队规范)把基础的单表操作生成出来,复杂查询再手写 XML。这样既能避免大量重复的样板代码,又保留手写 SQL 的灵活性,兼顾开发效率和查询可控性。
3. 数据库建模:旅游业务的核心表与字段设计逻辑
表结构设计决定了整个系统写起来顺不顺手。我在做这个系统时,没有急着写代码,而是先把核心表构造好,因为后端业务逻辑只是对表数据做操作,表设计烂了,后面每一步都难受。旅游系统的核心数据模型可以圈定为:管理员表、用户表、景点表、酒店表、线路表、攻略表、评论表,再加上关联表和常用辅助表。
3.1 核心表的核心字段设计
以景点表为例,我给出一份实际可用的表结构:
CREATE TABLE scenic_spot ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', name VARCHAR(100) NOT NULL COMMENT '景点名称', cover_image VARCHAR(255) COMMENT '封面图URL', images TEXT COMMENT '轮播图,逗号分隔', summary VARCHAR(500) COMMENT '一句话简介', content TEXT COMMENT '详细介绍', category VARCHAR(50) COMMENT '分类:自然/人文/乐园', location VARCHAR(255) COMMENT '具体地址', region VARCHAR(50) COMMENT '区域:汉滨/旬阳/岚皋等', open_time VARCHAR(100) COMMENT '开放时间', ticket_price DECIMAL(10,2) DEFAULT 0 COMMENT '门票价格', recommend TINYINT DEFAULT 0 COMMENT '是否推荐 0否 1是', view_count INT DEFAULT 0 COMMENT '访问次数', status TINYINT DEFAULT 1 COMMENT '状态 0下架 1上架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_region (region), KEY idx_category (category), KEY idx_recommend (recommend) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='景点信息表';几个值得留意的设计决策:
- 图片字段要存 URL 而不是二进制大数据,文件上传后返回一个资源链接,数据库中只存路径。前端展示时直接拼静态资源地址即可,这是最常见的实践方案
- 所有时间字段建议用 datetime 而非 timestamp,因为 timestamp 有 2038 年问题,而且 datetime 的语义更直观
- 状态字段用 tinyint 而不是字符串,既节省空间,查询也快
- 索引不是越多越好,但 region 和 category 这种高频查询条件字段加上索引很有必要
- 所有表的字符集统一为 utf8mb4,否则存储生僻字和表情符号会出问题
3.2 表间关系设计
旅游系统的表关系指数比较简单,但有几个关系要提前规划明白:
- 用户-评论-景点(或酒店/线路):用户表与景点表通过评论表产生多对多联系。评论表里的 target_type 字段可以用来区分评论的对象类型(1-景点,2-酒店,3-线路),这就是一个简单的“多态关联”设计
- 线路-景点:一条旅游线路会包含多个景点,一个景点也可能出现在多条线路中,所以需要一张关联表 line_scenic(线路ID + 景点ID),而不是在线路表里放一个景点的 JSON 数组
- 酒店-房型:一个酒店有多个房型,房型的 id 可以放进 hotel_room 表中,酒店表不存重复集合
这种关联表设计的好处是数据规范化程度高,不会产生冗余和更新异常。要知道,很多新人最容易犯的毛病就是在主表里塞一个字段存 1,2,3,4 这种逗号分隔的 ID 串,看起来查询方便,但统计和一致性维护会非常别扭。
3.3 数据库初始化与版本管理
开发过程中我强烈建议引入数据库脚本管理,也就是把建库建表语句放到项目根目录的 sql/ 文件夹里,每个版本存一个脚本文件。这样做有三个明显好处:一是团队成员可以随时重建本地库;二是部署生产环境时有据可查;三是避免大家在共享开发库上互相覆盖表结构。
开发时安装 MySQL 也很关键,遇到过不少安装了最新版却连不上控制台或者默认认证插件不兼容的坑。这里不建议用过新的版本,8.0 的社区版是一个相对稳妥的选择。下载完成后要注意设置字符集为 utf8mb4,并选对认证方式,否则后面程序连接数据库的时候容易报错。
4. 后端从空项目到跑通:SpringBoot + MyBatis 的配置与方法落地
后端工程的搭建虽然主要靠 IDEA 的初始化工具,但里面的每一步都需要知其所以然。这里我把最核心的配置和代码路径过一遍,全部是实际生产在用的写法。
4.1 项目初始化与依赖管理
用 IDEA 创建 Spring Initializr 项目,建议使用 Java JDK 1.8 或 11、SpringBoot 2.7.x 版本。不推荐用 SpringBoot 3.x 起步,因为 3.x 基于 Jakarta EE 规范,对 JDK 版本要求更高,部分老教程中的依赖写法也不适用。对新手来说,先用成熟稳定版本把项目跑通,再考虑升级,这是最现实的路径。
pom.xml 中核心依赖如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency> </dependencies>PageHelper 是我额外加上的分页插件,做后台列表分页非常好用,避免了手写 LIMIT 的重复劳动,也保证了分页 SQL 的统一性。Maven 里面这类的版本选择,建议跟着启动器的版本来,避免自身版本冲突。
4.2 application.yml 中的关键配置项
SpringBoot 的配置文件是项目启动的基础。以下是我日常在用的健康配置样式:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ankang_travel?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.ankang.travel.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl pagehelper: helper-dialect: mysql reasonable: true有几个点需要重点解释一下:
- allowPublicKeyRetrieval=true 这个参数在 MySQL 8.0 下经常需要加上,否则可能报公钥检索错误
- map-underscore-to-camel-case 这个配置特别有用,可以让数据库的 create_time 自动映射成实体的 createTime,减少大量手动映射
- serverTimezone=Asia/Shanghai 指定时区,否则连接数据库会报时差错误,这是中国开发者最常遇到的经典坑
- mapper-locations 指定 XML 文件位置,所有手写 SQL 放在这个目录下统一管理
4.3 Controller-Service-Mapper 三层结构的职责边界
后端代码的分层,本质上是为了把“接收请求、处理业务、访问数据”这三件事拆开,让代码好维护、好测试。我见过一些人为了图省事,把 SQL 写在 Controller 里,短期开发快,后期项目稍微扩展一点就崩溃。
推荐的分层方式是:
- Controller 层:接收参数、校验参数格式、调用 Service、包装返回结果。这一层不写 SQL,不写复杂业务逻辑
- Service 层:承载业务逻辑,比如用户注册时要检查用户名是否存在、创建评论时要更新景点评论数,这些流程编排都在这一层
- Mapper 层(DAO):只负责数据库的增删改查,方法名和参数要与 XML 中的 SQL 对应
以景点列表接口为例:
@RestController @RequestMapping("/api/scenic") public class ScenicController { @Resource private ScenicService scenicService; @GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, ScenicQuery query) { PageInfo<ScenicSpot> page = scenicService.queryPage(pageNum, pageSize, query); return Result.success(page); } }Service 层再调用 Mapper 层,Mapper 在 XML 中编写动态 SQL:
<select id="selectPage" resultType="com.ankang.travel.entity.ScenicSpot"> SELECT * FROM scenic_spot <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="region != null and region != ''"> AND region = #{region} </if> <if test="category != null and category != ''"> AND category = #{category} </if> AND status = 1 </where> ORDER BY create_time DESC </select>动态 SQL 的 where 标签自动处理了 AND 前缀问题,这是 MyBatis 中高频使用的技巧,酒店筛选、线路筛选都可以按这个模式扩展。
4.4 文件上传与静态资源映射
旅游系统的后台涉及图片上传,实际开发中一个相对独立的做法是本地磁盘保存 + 虚拟路径映射。实现上,定义一个上传目录,接收 MultipartFile,生成 UUID 文件名,保存后返回 /uploads/xxx.jpg 这个 URL。然后写一个 WebMvcConfigurer 把 /uploads/** 映射到本地磁盘目录。
这个方案最大的优势是部署简单,不需要引入 MinIO 或云存储。但要注意两点:一是服务器上上传目录要提前创建好,并给足写权限;二是 Nginx 部署时要把 /uploads/ 这个路径也放进静态资源访问规则,这样前端才能在页面上真正显示图片。不要踩这种最常见的前端图片 404 的坑。
4.5 登录校验与拦截器实现
登录校验用一个拦截器来实现就够了,不需要上 Spring Security 这种重型框架。在 interceptor 内排除掉 /api/user/login、/api/scenic/, /api/hotel/等游客可访问的接口,管理员的 /api/admin/** 则必须登录后才能访问。
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user = request.getSession().getAttribute("loginUser"); if (user == null) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}"); return false; } return true; } }Session 方案在单机部署下足够用。如果以后系统要横向扩展、部署多个实例,再考虑把 Session 换成 Redis,配合 Token 传递身份信息。
5. 前端 Vue 接入:从环境配置到管理后台页面搭建
前端部分,很多做后端的人会在这一步卡壳,原因不是说 Vue 有多难,而是对前端的工程流程不熟。环境配置、依赖安装、代理转发、打包部署,这一整套链路从一开始就要跑通。
5.1 Node.js 环境与项目初始化
前端工程依赖 Node.js 环境。安装时我建议选用 LTS 长期支持版本,具体版本号没有严格要求,但不要太新,否则某些依赖包还没跟上会报兼容错误。安装完成后,用 Node 自带的 npm 工具来管理依赖。
这里有一个新手必踩的坑:直接下载依赖时,npm 速度极慢,很容易卡在某个大包上,这是因为默认的源服务器在国外。解决办法是切换到国内镜像源:
npm config set registry https://registry.npmmirror.com执行之后,再运行 npm install 就会发现速度有了质的提升。如果项目创建时使用的是 Vue CLI,还可以用对应的图形界面工具查看依赖状态。
我自己的建议是使用 Vite 创建 Vue 3 项目。Vite 的启动速度比 Webpack 快不少,开发时热更新非常爽,配置也简洁。如果你特别依赖老教程,选 Vue 2 + Vue CLI 也没问题,原理是相通的。
5.2 接口请求封装与代理转发
前端调用后端接口时,统一封装一个 axios 实例,把 baseURL 和拦截器配置好:
import axios from 'axios' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res }, error => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default servicebaseURL 设置为相对路径 /api,开发环境通过 Vite 的 proxy 配置把请求转发到后端端口,生产环境由 Nginx 统一转发,这样前后端联调和部署都不用改前端代码里的接口地址,非常方便。以下是 Vite 的代理配置:
export default defineConfig({ server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })5.3 后台管理页面的表单与表格实现
后台管理的本质就是“表格+表单”的循环。列表页用 Element Plus 的 el-table 展示数据,配合 el-pagination 分页组件;新增/编辑页用 el-dialog 弹窗嵌套 el-form 表单;搜索区域用 el-form inline 模式。
路由配置方面,后台建议做成独立的 layout,包含侧边栏菜单和顶部导航栏,子页面通过 router-view 切换。你不需要给每个页面写重复的头部和侧边栏代码,只需要在根路由下嵌套子路由即可。
在这个过程中要注意的是表单校验规则、日期格式化、状态枚举转换(比如 1 转成“上架”、0 转成“下架”),这些是后台系统最容易出细节问题的地方。状态转换写好过滤器之后,整个后台的观感会专业很多。
5.4 前台门户的视觉与交互要点
前台面向游客,视觉呈现直接影响体验,也直接影响老师或面试官对项目的初印象。首页的轮播图可以用 Element Plus 的 el-carousel 或 Swiper 实现,景点卡片展示图片和名称,点击跳转详情。列表页的筛选器放在顶栏或侧边栏,下拉选项的数据由后端接口动态返回,不要写死。
如果你希望在项目中增加一点技术亮点,可以考虑在景点详情页接入视频播放模块,比如播放景区的宣传视频。视频转码后生成 m3u8 格式的切片文件,前端用 vue 配套的视频播放器插件加载 m3u8 地址,这样画面更现代。不过要注意,这个模块不是核心功能,属于加分项,放在项目迭代的后期再来做。
前端打包后有一个常见坑:打包后的静态资源路径不对导致图片、CSS 加载失败。解决方法是修改 Vite 或 Vue CLI 的 publicPath 配置,将其设置为相对路径或者 Nginx 中以子路径部署的路径。Nginx 部署时需要注意这里。
6. 前后端联调阶段最容易翻车的几类问题
到了联调阶段,真正的排障工作才开始。前后端各自开发的时候一切正常,一联调各种问题就冒出来了。这里我列出几类高频问题,每一条都是项目里真实踩过的坑。
6.1 跨域请求怎么处理
跨域问题出现的原因很直接:前端页面运行在 localhost:3000,后端接口在 localhost:8080,浏览器基于同源策略会拦截跨域请求。解决办法有几类,做开发阶段最推荐在 SpringBoot 中配置 CORS 映射:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }注意允许凭证请求时,allowedOriginPatterns 推荐用模式而不是直接用 allowedOrigins("*"),否则浏览器可能报错。
还有一种更彻底的办法是走 Nginx 反代,把 /api 路径代理到后端地址,从浏览器视角看所有请求都指向同一个源,压根没有跨域问题。这正好说明前后端分离项目用 Nginx 部署是顺理成章的方案。
6.2 日期与时间格式不一致
后端返回给前端的日期默认可能是时间戳或者带 T 的 ISO 字符串,前端展示的时候要么不对,要么干脆显示成 NaN。规范的解决办法是后端统一日期序列化格式,也就是前面 application.yml 里配置的:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8这样后端返回的 JSON 字符串就是“2025-01-15 14:30:00”这种格式,前端直接展示即可。如果某些字段前端想显示成 yyyy-MM-dd,那就在前端做格式化处理,封装一个全局过滤器。这就是分层的意义。
6.3 分页插件的返回值包装问题
PageHelper 使用后返回的是 Page 对象,它继承自 ArrayList,不建议直接把这个 Page 对象作为接口返回值返回给前端,因为 JSON 序列化时会把分页信息带出来但格式很丑。更加规范的做法是 Service 层直接返回 PageInfo 对象,前端通过 pageInfo.list 取数据、通过 pageInfo.total 取总数:
PageHelper.startPage(pageNum, pageSize); List<ScenicSpot> list = scenicSpotMapper.selectPage(query); PageInfo<ScenicSpot> pageInfo = new PageInfo<>(list); return Result.success(pageInfo);前端拿到数据后,分页组件的 current 和 total 直接对应上。如果你用 MyBatis-Plus,分页方式略有不同,但概念是相通的。联调的时候碰到数据与总数对不上的情况,十有八九是分页插件没生效或 PageHelper 与多数据源有冲突。
6.4 参数校验与空值防御
接口联调时,另一个高发问题是参数校验不严格。前端传了一个空字符串或者 null,后端没有做处理,SQL 查询就异常,或者数据库插入了脏数据。这里我推荐两个层面的处理:
后端层面,对必填参数使用 JSR-303 注解(比如 @NotNull、@NotBlank),或者在业务逻辑里显式判断并返回友好提示。数据库层面,给关键字段加上 NOT NULL 约束和 DEFAULT 默认值。不要只依赖前端校验,前端校验只是为了体验,后端校验才是安全底线。
比如注册接口的密码字段,不仅要判空,还要按业务规则校验长度。这些代码写起来不复杂,但体现的是工程素养,面试时也很容易被问到。
7. 打包部署:从开发机到服务器的最后一公里
项目开发到了尾声,最后一步就是部署上线。这一部分在许多教学视频里是被忽略的,但我认为恰恰是完整项目中含金量比较高的环节,经历过的人会对整个系统有更全面的认知。
7.1 前端构建与 Nginx 配置
前端构建很直接,在 Vue 工程根目录执行 npm run build,就会生成一个 dist 静态资源目录。将这个目录里的内容上传到服务器的指定目录,比如 /usr/share/nginx/ankang,然后配置 Nginx:
server { listen 80; server_name your_domain; root /usr/share/nginx/ankang; index index.html; 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 /uploads { alias /usr/local/ankang/uploads; } }try_files 配置是 Vue 路由在 history 模式下刷新页面 404 问题的最优解。它的作用是当请求的路径不是真实文件时,自动回退到 index.html,让前端路由接管。这个配置不加,你会发现首页能打开,但一旦刷新子页面就白屏了。
7.2 后端打包与启动
后端项目在 IDEA 中执行 mvn clean package 即可构建一个可执行的 jar 包。构建时如果遇到测试用例失败导致打包中断的问题,可以加参数跳过测试:
mvn clean package -DskipTests上传 jar 包到服务器,用如下命令启动:
nohup java -jar ankang-travel-0.0.1-SNAPSHOT.jar --server.port=8080 > app.log 2>&1 &nohup 和 & 的组合让进程在 SSH 断开后依然运行,日志输出到 app.log。如果需要查看日志,tail -f app.log 即可。要注意的是,服务器上的 MySQL 需要在启动前初始化好,并导入项目根目录 sql/ 下的脚本文件。数据库连接参数通过启动命令覆盖是运维时比较方便的方式,也可以写入 application-prod.yml 再用 --spring.profiles.active=prod 指定。
7.3 负载问题与持续优化方向
项目如果只做演示,到这一步就已经完成了。但如果想继续优化并积累经验,可以从这几个方向入手:
- 数据量变大后,景点的热门列表接口可以做 Redis 缓存,key 设计成 scenic:recommend:top10,缓存时间 10 分钟,减少数据库压力
- 图片上传后的体积优化:前端做图片压缩,后端做尺寸裁剪或引入 CDN
- 给后台接口加操作日志,记录管理员对景点、酒店、攻略的每次修改,这是管理系统常见的进阶需求
- 数据库连接池参数调优,连接池初始大小、最大连接数要根据服务器的硬件配置来设
这些扩展点没有一样是花哨的,全是真实项目中会碰到的实际问题。如果你在毕业设计或者面试中能把其中一两个点讲明白,会明显提升整个项目的技术含金量。
我个人在实际开发中的体会是:这类管理系统最大的价值不在于技术有多新,而在于你是否真的把每一个环节跑通了。关于 Vue 打包后布局异常的问题,我曾排查了很久,最后发现是 index.html 的资源引用路径少了一个点,这种小问题在部署阶段最容易出现,遇到了一定要耐心看控制台和网络面板,逐一排查。
整个系统从表设计到前端页面,再到打包部署,所有流程都走顺之后,你会对整个全栈项目有一种豁然开朗的感觉。SpringBoot 负责把复杂配置留给自己、Vue 负责把交互体验交给浏览器、MySQL 和 MyBatis 负责把数据管得明明白白。以后不管碰到什么业务类型的管理系统,拿到手的第一反应都能快速拆出“数据模型 + 接口设计 + 页面构建”这三条线,这才是这类练习项目真正留给你的能力。