敲完喀什旅游网站这套系统最后一行代码的时候,我的感受是:这种带真实业务场景的 Java Web 项目,比那些纯电商、纯管理后台练手项目有价值得多。技术栈就是标题里那套——SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0,前后端分离,从零到一完整落地,还配了设计文档。这篇文章我把整个项目从头到尾拆开讲一遍,包括技术选型为什么这么定、数据库表怎么设计、后端接口怎么组织、前端页面怎么搭建、联调部署时踩了哪些坑。无论是正在做毕设,还是想找一个能写进简历的实战项目,这套东西都值得你花十分钟认真看看。
1. 项目从哪来:喀什旅游网站的需求背景与技术选型
1.1 旅游类网站的真实业务逻辑
喀什这个地方本身就很有话题性,喀什古城、香妃园、艾提尕尔民俗区、帕米尔高原、慕士塔格峰,再加上独特的美食和手工艺文化,旅游资源相当密集。所以旅游网站这个选题不是凭空造的,它有清晰的使用人群和业务场景:游客需要查景点、看线路、订酒店、找美食、读攻略;运营人员需要维护景点信息、发布资讯、处理用户订单;管理员还要管用户、管评论、看数据统计。这就是一个典型的"内容管理 + 交易流程 + 用户体系"的组合,非常适合用来做 Java Web 方向的项目展示。
我开发的时候把用户角色拆成了三类:普通游客(前台浏览)、注册用户(登录后可以收藏、评论、下单)、后台运营管理员(维护内容、处理订单)。这个角色划分几乎能覆盖所有景区类网站的通用模型,后面做小程序或者 App 的时候,后端接口基本可以直接复用,只换前端壳子就行。
1.2 技术栈选型:这套组合为什么稳
技术上我几乎是刻意选了这套最主流的组合,原因很实在:资料多、社区大、踩坑成本低。
SpringBoot2 到现在依然是国内中小型项目和生产环境里的主力版本,网上教程、面试题、现成组件都比比皆是,而且它对 JDK8 的支持非常完美,不用为了新特性去折腾环境。Vue3 这代框架的 Composition API 写业务逻辑比 Options API 清晰得多,配合 Vite 开发体验提升不是一点半点,Vue3 的生态现在也相当成熟了,Element Plus 这些组件库跟上之后,做后台管理系统几乎就是拼积木。MyBatis-Plus 是 MyBatis 的增强包,单表 CRUD 基本不用自己写 SQL,条件构造器做动态查询非常顺手,是我个人强烈推荐的东西。MySQL8.0 就不用多说了,性能、JSON 支持、窗口函数都比 5.7 强不少,Ubuntu、CentOS、Windows 都有很成熟的安装教程,环境搭建难度不大。
我整理了一张选型对照表,方便你理解当时为什么这么定:
| 层面 | 选用方案 | 核心理由 |
|---|---|---|
| 后端框架 | SpringBoot 2.7 | 生态成熟、自动化配置、部署简单 |
| 持久层 | MyBatis-Plus 3.5 | BaseMapper 免写 SQL、Wrapper 动态条件、分页插件 |
| 前端框架 | Vue3 + Vite | Composition API 更利于逻辑复用、Vite 冷启动极快 |
| UI 组件 | Element Plus | 中后台组件全、文档完善、Vue3 原生适配 |
| 数据库 | MySQL 8.0 | utf8mb4 完整支持、性能更好、窗口函数可用 |
| 鉴权方案 | JWT + 拦截器 | 前后端分离场景下无状态鉴权最省事 |
这套组合还有一个好处:一旦跑通了,你以后做任何管理系统、门户网站、小程序后端,都是同样的套路,等于一套技能吃遍大部分常见需求。
2. 整体架构与核心功能拆解
2.1 前后端分离的工程结构
项目采用前后端分离架构,后端单独起 SpringBoot 服务,提供 JSON 接口;前端是独立的 Vue3 工程,通过 HTTP 请求调接口。这么做的好处非常多:前后端并行开发互不阻塞;将来要加小程序、App、H5,前端随便换,后端完全不用动;部署的时候也能把静态资源放到 Nginx 或者云存储上,分担应用服务器压力。
后端工程里我按功能模块分包,典型目录结构大致是:
src/main/java/com/kashi/travel ├── common // 统一返回结果、全局异常、常量 ├── config // MyBatis-Plus 分页配置、WebMvc 拦截器配置 ├── controller // 前台门户、后台管理的 Controller ├── entity // 数据库实体类 ├── mapper // MyBatis-Plus 的 Mapper 接口 ├── service // 业务逻辑层 ├── util // JWT、日期处理等工具类 └── KashiTravelApplication.java前端用 Vite 创建的 Vue3 工程,目录按 views、components、api、router、store 划分。开发阶段通过 Vite 的 proxy 把 /api 开头的请求转发到 SpringBoot 的 8080 端口,生产环境就由 Nginx 统一处理反向代理,这个标准姿势我在后面部署章节细说。
2.2 前台门户:游客能看到什么
前台是这个项目的门面,页面设计上要突出旅游网站的"展示和引导"属性。我重点做了这些页面模块:
首页是门户的核心,顶部导航有首页、景点、线路、酒店、美食、攻略、关于我们。首页的 Banner 轮播图用来放喀什的标志性景观;下方是热门景点推荐,后台运营可以设置推荐排序;接着是精选线路板块,用卡片形式展示路线名、天数、价格;底部展示了最近发布的旅游攻略。
景点列表页和线路列表页都支持分类筛选,景点页有搜索按名称或地区筛选,线路页按"天数区间"和"价格区间"两个维度筛,这样比单纯的关键词搜索更贴近真实用户需求。详情页则是信息展示的重头戏:景点详情页展示图文介绍、开放时间、门票价格、地理位置;线路详情页展示行程安排表格、费用包含、预订须知。
用户模块包含注册、登录、个人中心。游客区分游客和注册用户,收藏、评论、下单都必须登录。个人中心里能看到我的订单、我的收藏、我的评论,这样核心业务闭环就完整了。
2.3 后台管理:让运营人员省心
后台单独跑在 /admin 路由下面,是典型的元素化管理界面。我把后台功能分为四大块:
景点管理负责景点信息的增删改查,包括图片地址、分类、简介、开放时间、门票价格、推荐状态。线路管理负责旅游线路的维护,重点是把行程明细做成列表存到 JSON 字段里,前端自动渲染成行程表。资讯管理用来发布旅游攻略和公告,编辑器存富文本 HTML,前端详情页直接 v-html 渲染。订单管理是业务流程的关键,用户可以下单预订线路,后台按状态(待支付、已支付、已取消、已完成)过滤,还能看到关联的下单人信息和下单时间。
后台还加了一个简单的数据看板,首页展示景点总数、注册用户数、待处理订单数,这些数据其实就是几条 MyBatis-Plus 的 selectCount 查询,拼在一起做成了简化版仪表盘。虽然不复杂,但是非常直观地体现出了"后台是给业务人员用的"这个设计思路。
3. 数据库设计与核心表结构实现
3.1 核心表关系梳理
数据库是整个系统的地基,表设计得好不好,直接决定后面写代码是享受还是受罪。我画的表结构关系大致是这样的:用户表 user 是最外层的参与主体,它对景点、线路的收藏产生了 favorite 关联表;用户对线路的预订产生了 order 订单表;用户对景点和线路的评论产生了 comment 表。内容端则是四张平行表:景点表 scenic_spot、线路表 travel_route、酒店表 hotel、美食表 food,再加上资讯文章表 article,就构建起了完整的内容体系。
设计时我刻意尽量避免多对多关系的中间表泛滥,收藏用了 unique 索引约束防止重复收藏;订单表直接冗余了线路名称和下单用户昵称,查询列表的时候少关联两张表,对性能是很大的帮助。这是我在实际项目里很强调的一个习惯:为了查询方便,适度冗余字段是可以接受的。
3.2 MySQL8.0 下的建表细节与踩坑
MySQL8.0 环境下建表,有几个细节千万不要偷懒。第一,字符集一定要指定 utf8mb4,不要用默认的 utf8mb3,否则存 emoji 表情(比如评论里用户发个 或者特殊符号)会报错或者变问号。第二,所有表都用 InnoDB 引擎,MyISAM 在事务和崩溃恢复方面完全比不上。第三,时间字段我统一用 datetime 类型,状态字段用 tinyint,金额字段用 decimal(10,2),不用 float 做金额,因为浮点数的精度问题在金额计算上是不可以接受的。
这里给一张核心表的精简结构,方便你看清字段设计思路:
| 表名 | 关键字段 | 用途说明 |
|---|---|---|
| user | id, username, password, nickname, avatar, phone, role | 用户表,password 存 BCrypt 密文 |
| scenic_spot | id, name, cover, images, category, region, description, open_time, ticket_price | 景点表,images 存 JSON 数组 |
| travel_route | id, name, cover, days, price, highlights, schedule, status | 线路表,schedule 存 JSON 行程安排 |
| order | id, order_no, user_id, route_id, route_name, user_name, contact_phone, status, create_time | 订单表,冗余了两个名称字段 |
| favorite | id, user_id, target_type, target_id, create_time | 收藏表,通过 target_type 区分景点/线路 |
| article | id, title, cover, content, author_id, view_count, publish_time | 攻略资讯表,content 为富文本 |
建表语句里有一个典型片段可以给你参考,约定了创建时间和更新时间,后面做排序和数据统计就方便了:
CREATE TABLE `scenic_spot` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '景点名称', `cover` varchar(255) DEFAULT NULL COMMENT '封面图', `images` json DEFAULT NULL COMMENT '图集JSON', `category` varchar(50) DEFAULT NULL COMMENT '分类', `region` varchar(100) DEFAULT NULL COMMENT '所在区域', `description` text COMMENT '详细介绍', `open_time` varchar(100) DEFAULT NULL COMMENT '开放时间', `ticket_price` decimal(10,2) DEFAULT '0.00' COMMENT '门票价格', `recommend` tinyint(1) DEFAULT '0' COMMENT '是否推荐', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_region` (`region`), KEY `idx_recommend` (`recommend`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='景点表';MySQL8.0 的 json 类型用在场景图集、行程安排这种结构不固定的字段上非常合适,用 MyBatis-Plus 可以配合 Jackson 的 ObjectMapper 做自动映射,存取数组对象都很顺畅。不过要注意,JSON 字段不能走索引,如果业务上有按内部元素查询的需求,就要考虑抽出来单独建关联表了。
4. 后端落地:从 Controller 到 Mapper 的完整链路
4.1 统一返回结果与全局异常处理
接口设计上,所有接口的返回值我都统一成一个 Result 对象,字段是 code、message、data,成功 code 为 200,业务错误用其他码,未登录是 401,无权限是 403。前端 axios 拦截器拿到这个结构后,统一判断 code,不用在每一个请求里重复写错误分支,省掉了海量样板代码。
我在开发中最推荐的做法是先写好一个泛型 Result 类,再配合一个全局异常处理器,所有业务异常都通过抛出异常的方式传递,而不是层层返回 null 去判断。比如用户注册时用户名已存在,就直接抛一个 BizException("用户名已被注册"),异常处理器自动捕获并返回给前端。这样 Controller 层的方法非常干净,可读性和可维护性都强很多。
4.2 MyBatis-Plus 的实战:条件构造器、分页与自动填充
MyBatis-Plus 这个增强库是这套项目里我最满意的部分。Mapper 接口只需要继承 BaseMapper,就自动拥有了单表的 insert、delete、update、selectById、selectList 这些能力,基础 CRUD 几乎零 SQL。复杂一点的查询用 LambdaQueryWrapper 构造条件,代码既安全又直观:
LambdaQueryWrapper<ScenicSpot> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ScenicSpot::getRegion, "喀什古城") .eq(ScenicSpot::getRecommend, 1) .orderByDesc(ScenicSpot::getCreateTime); List<ScenicSpot> list = scenicSpotMapper.selectList(wrapper);列表页的分页用的是 MyBatis-Plus 提供的分页插件,配置一个 MybatisPlusInterceptor 加 PaginationInnerInterceptor,再指定数据库类型为 mysql,之后所有分页查询都是标准的 Page 对象。前端传 current 和 size 两个参数,后端返回 total、pages、records,前端配合 Element Plus 的 el-pagination 组件直接对接,过程非常顺滑。
这里有一个我后来看很多人都踩过的坑:分页插件没有生效时,页面上显示的数据可能是一整页全部返回,或者 total 恒定为 0。排查思路一般是确认配置类有没有扫描到、数据库类型有没有写错,以及最重要的一点,分页插件拦截器需要和事务管理器配合,不要在同一个方法里先开启事务再查询,否则分页有可能失效。
自动填充功能也值得用起来。我在 createTime 和 updateTime 字段上加了 @TableField(fill = FieldFill.INSERT) 这样的注解,然后定义一个 MetaObjectHandler 实现类,在 insert 和 update 时自动填充当前时间。这样业务代码里完全不用手动 set 时间,统一性非常好。
4.3 登录鉴权与接口安全
用户登录这里我选了 JWT,而不是传统的 Session。原因是前后端分离之后,Session 的 Cookie 跨域问题比较麻烦,JWT 直接把用户信息加密放在 token 里,前端每次请求在 Header 中带上 Authorization,后端拦截器解析校验,无状态、好扩展,非常契合 Vue3 前端工程的开发方式。
密码存储用了 BCrypt 加密。注意前端传过来的明文密码,后端要用 BCryptPasswordEncoder 先加密再入库,任何时候都不要明文存密码,也不要自己写一套加密算法。BCrypt 自带随机盐,同一个密码每次加密结果都不同,安全性远高于 MD5 加固定盐的做法。登录时用 matches 方法比对,密码错误就不给发 token。
拦截器做法也在项目里体现得很到位。我注册了一个 HandlerInterceptor,在 preHandle 里校验请求头里的 token,校验通过就把用户 ID 和角色信息放进 ThreadLocal 里,供后续业务使用。后台管理接口额外校验 role 字段,非管理员直接返回 403。这一套下来,前后端权限模型就非常清晰了。
5. 前端落地:Vue3 工程化实战
5.1 Vite 工程的结构与配置
前端我用的 Vite 创建项目,命令很简单:npm create vite@latest travel-web -- --template vue。相比 Webpack,Vite 开发环境基于原生 ES Module,冷启动几乎不用等,改代码后的热更新也是毫秒级的,日常开发体验确实好很多。上线前执行 npm run build 做生产构建,产物就是纯静态文件,交给 Nginx 托管即可。
工程目录我是这么约定的:views 放页面级组件;components 放通用业务组件;api 目录按模块拆分请求方法,每个模块一个 js 文件;router 定义路由表和守卫;store 用 Pinia 管理用户状态。约定大于配置这套规矩在前端项目里特别重要,可以让新加入的人快速定位代码,不用靠猜。
路由守卫是这里的一个核心环节。未登录用户访问个人中心或者进入后台,路由守卫会拦截并跳转到登录页。后台路由统一挂在 meta.requiresAdmin 标记下,管理员校验不通过就弹提示并跳出去。这种以路由为入口的权限控制,比在页面里反复判断用户状态要省心得多。
5.2 从首页到下单:核心页面实现拆解
这里我挑三个有代表性的页面模块讲一下实现思路,你可以拿着这些思路直接套用到自己项目里。
首页 Banner 轮播用了 Element Plus 的 el-carousel,图片地址来自后台配置好的 recommendation 接口,返回一个包含图片、标题、跳转链接的数组。热门景点板块用卡片栅格布局,el-row + el-col 做响应式,一行三列,每张卡片展示封面、名称、区域和门票价格,点击跳转详情页。
线路详情页是我理想中旅游网站页面里最关键的。页面顶部是主图和线路基本信息,中间用 el-tabs 切换行程安排、费用说明、预订须知三个标签页。行程安排是接口返回的 JSON 数组格式,用 el-table 渲染出第几天、行程内容、住宿餐饮三列,直接解决了富文本编辑器排版行程不统一的问题。
下单流程是用户核心操作。用户选好线路后点击"立即预订",弹窗让填写联系人姓名、手机号。提交时前端先校验手机号格式,再调用新增订单接口。后端创建订单后返回订单号和支付状态,前端跳转到订单详情页并提示"待支付"。这里我刻意没有接入真实支付,订单状态支持手动模拟支付,毕设和项目演示完全够用了。
5.3 axios 封装与前后端联调
axios 封装是我每次写 Vue 项目必做的一步。全局创建一个 axios 实例,baseURL 设为 /api,请求拦截器里把本地存储的 token 塞到 Authorization 头,响应拦截器统一返回 code 为 200 的 data,其他 code 根据业务码弹出对应提示。遇到 401 就清掉本地 token 并跳转登录页,用户感知更加平滑。
开发环境跨域问题很容易卡住新人。前后端端口不同,浏览器同源策略会阻止请求。我的方案是在 Vite 配置文件里加 server.proxy 代理:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }这样前端发出的 /api 前缀请求在开发服务器就被转发到了后端,浏览器不感知跨域,无需在后端额外开启 CORS。生产环境由 Nginx 配置 location /api 的反向代理,逻辑完全一致。这套联调方式是我跑过多个项目后总结出的最省心方案。
6. 部署上线与问题排查速查表
6.1 本地启动全流程
把项目从源码跑起来这套流程,我希望每个拿到项目的人都能十分钟内操作完。首先准备环境:JDK8+、Maven3.6+、Node16+、MySQL8.0。MySQL8.0 的安装各个平台教程都很完善,核心是记住字符集选 utf8mb4,服务端口保持默认 3306。启动 MySQL 后,创建数据库 travel_db,执行项目里 database 目录下的建库建表 SQL,然后配置 src/main/resources/application.yml 里的数据库连接串,改成你自己的账号密码。
后端启动最简单,命令行进入项目目录执行 mvn spring-boot:run,或者用 IDEA 导入 Maven 工程后直接运行主类。看到 Tomcat started on port 8080 就说明后端起来了。前端在命令窗口执行 npm install 装依赖,然后 npm run dev,Vite 默认会开启 5173 端口,浏览器访问 http://localhost:5173 即可。开发模式下所有 /api 请求都通过代理转发到后端,所以不需要任何额外配置。
6.2 MySQL8.0 的几个典型坑
我第一次迁移项目到 MySQL8.0 时,在启动阶段就被卡了好几次,这里把这些坑一次性列出来。第一,驱动类变了,早期用 com.mysql.jdbc.Driver,MySQL8.0 必须用 com.mysql.cj.jdbc.Driver,数据源配置里不要写错。第二,时区问题,MySQL8.0 默认时区可能跟本地不一致,连接串里加上 serverTimezone=Asia/Shanghai,避免报错和存储的时间差 8 小时。第三,如果连的是远程数据库,记得检查 allowPublicKeyRetrieval=true 这个参数,否则可能出现 Public Key Retrieval is not allowed 的认证问题。
还有一个容易被忽略但很影响体验的是:MySQL8.0 默认的密码认证插件是 caching_sha2_password,一些老版本的客户端连接器不兼容,如果遇到握手失败就更新连接器版本,不要在数据库里把认证插件改成 mysql_native_password,后患无穷。
6.3 部署上线与后续扩展
上生产环境前,前端先执行 npm run build,把 dist 目录下的静态资源复制到服务器上。后端用 Maven 打成 jar 包,服务器只需要装 JDK 和 MySQL,不用装额外的应用中间件。推荐部署方式是 Nginx 同时做静态资源托管和反向代理,把域名的根路径指向前端 dist,把 /api 开头的请求 proxy 到后端端口:
server { listen 80; server_name your-domain.com; location / { root /var/www/travel-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; } }n 线上部署有几个细节提醒你:前端路由模式如果用 history 模式,务必配置 try_files 匿名到 index.html 的规则,否则刷新二级页面会 404;后端打包时注意静态资源大小,图片建议压缩后再上传,不要直接拿大图跑。这套项目后续如果要扩展小程序端,后端接口几乎可以直接复用;如果要做多商户平台,把订单表和商户表关联起来就能演化出更复杂的业务模型。
最后说一下我个人的体会:做旅游类 Java Web 项目,最重要的不是把技术栈堆得多花哨,而是能不能把"内容展示 → 用户访问 → 收藏评论 → 预订下单"这条业务链完整闭环。这套项目做到了,而且每一步的实现思路都清晰直接,所有环节都能在文档里找到对应说明。我的建议是,拿到源码后不要急着改功能,先花两天把数据库表结构和接口列表过一遍,然后再按前文档的流程把项目跑通,最后再根据自己的需求去改逻辑。这个顺序能帮你少走很多弯路,也是我复盘整个项目后最想强调的一点。