ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue3+MyBatis-Plus+MySQL8.0实战:旅游网站系统开发全解析

SpringBoot+Vue3+MyBatis-Plus+MySQL8.0实战:旅游网站系统开发全解析 敲完喀什旅游网站这套系统最后一行代码的时候我的感受是这种带真实业务场景的 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.5BaseMapper 免写 SQL、Wrapper 动态条件、分页插件前端框架Vue3 ViteComposition API 更利于逻辑复用、Vite 冷启动极快UI 组件Element Plus中后台组件全、文档完善、Vue3 原生适配数据库MySQL 8.0utf8mb4 完整支持、性能更好、窗口函数可用鉴权方案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 做金额因为浮点数的精度问题在金额计算上是不可以接受的。这里给一张核心表的精简结构方便你看清字段设计思路表名关键字段用途说明userid, username, password, nickname, avatar, phone, role用户表password 存 BCrypt 密文scenic_spotid, name, cover, images, category, region, description, open_time, ticket_price景点表images 存 JSON 数组travel_routeid, name, cover, days, price, highlights, schedule, status线路表schedule 存 JSON 行程安排orderid, order_no, user_id, route_id, route_name, user_name, contact_phone, status, create_time订单表冗余了两个名称字段favoriteid, user_id, target_type, target_id, create_time收藏表通过 target_type 区分景点/线路articleid, 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) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 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 构造条件代码既安全又直观LambdaQueryWrapperScenicSpot wrapper new LambdaQueryWrapper(); wrapper.eq(ScenicSpot::getRegion, 喀什古城) .eq(ScenicSpot::getRecommend, 1) .orderByDesc(ScenicSpot::getCreateTime); ListScenicSpot 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 vitelatest travel-web -- --template vue。相比 WebpackVite 开发环境基于原生 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 devVite 默认会开启 5173 端口浏览器访问 http://localhost:5173 即可。开发模式下所有 /api 请求都通过代理转发到后端所以不需要任何额外配置。6.2 MySQL8.0 的几个典型坑我第一次迁移项目到 MySQL8.0 时在启动阶段就被卡了好几次这里把这些坑一次性列出来。第一驱动类变了早期用 com.mysql.jdbc.DriverMySQL8.0 必须用 com.mysql.cj.jdbc.Driver数据源配置里不要写错。第二时区问题MySQL8.0 默认时区可能跟本地不一致连接串里加上 serverTimezoneAsia/Shanghai避免报错和存储的时间差 8 小时。第三如果连的是远程数据库记得检查 allowPublicKeyRetrievaltrue 这个参数否则可能出现 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 项目最重要的不是把技术栈堆得多花哨而是能不能把内容展示 → 用户访问 → 收藏评论 → 预订下单这条业务链完整闭环。这套项目做到了而且每一步的实现思路都清晰直接所有环节都能在文档里找到对应说明。我的建议是拿到源码后不要急着改功能先花两天把数据库表结构和接口列表过一遍然后再按前文档的流程把项目跑通最后再根据自己的需求去改逻辑。这个顺序能帮你少走很多弯路也是我复盘整个项目后最想强调的一点。
返回列表