SpringBoot+Vue前后端分离项目实战:从零搭建红色旅游系统 这类毕业设计项目最值得关注的不是功能有多全而是能不能把前后端技术栈跑通、把核心业务流程走顺并且能清晰地向导师展示你的技术选型、架构设计和实现细节。SpringBoot Vue 的组合是当前企业级开发的主流用它来做“红色革命老区旅游系统”这类主题既能体现技术应用能力又能结合具体业务场景是一个不错的毕设选题方向。但很多同学在动手时会陷入两个误区一是过度追求功能复杂导致后期代码混乱、难以收尾二是只关注界面忽略了后端数据流转、接口设计和数据库建模这些更体现功力的部分。我更建议你把重点放在“如何用最清晰的架构实现一个可演示、可讲解、代码结构良好的系统”上。下面我会按照一个真实项目从零搭建到核心功能实现的顺序拆解整个流程。我会重点讲环境准备、前后端分离架构的搭建、核心业务模块的实现以及那些最容易卡住你的部署和调试环节。目标是让你能照着步骤做出一个具备完整增删改查、用户权限和前后端联调功能的可运行系统。1. 环境准备与项目初始化别在第一步就卡住开始写代码之前先把环境配好。很多“项目跑不起来”的问题根源都在环境上。1.1 后端环境JDK、Maven、MySQL 与 IDEA后端我们使用 SpringBoot 2.7.x一个相对稳定且资料丰富的版本数据库用 MySQL 8.0 或 5.7。1. JDK必须使用 JDK 8 或 JDK 11这是 SpringBoot 2.7.x 官方长期支持的版本。不建议用最新的 JDK 17 或 20可能会遇到一些依赖库兼容性问题。安装后在命令行输入java -version和javac -version确认版本。2. Maven用于管理项目依赖和打包。去官网下载解压后配置环境变量MAVEN_HOME和PATH。在命令行输入mvn -v能显示版本信息即表示成功。我建议在~/.m2/settings.xml文件中配置阿里云的镜像仓库能大幅加快依赖下载速度。mirror idaliyunmaven/id mirrorOf*/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror3. MySQL安装 MySQL 8.0记住你设置的 root 密码。安装完成后创建一个专门用于本项目的数据库比如red_tourism字符集用utf8mb4排序规则用utf8mb4_general_ci。4. IDEAIntelliJ IDEA这是开发 SpringBoot 项目最主流的 IDE。确保安装了 Lombok 插件用于简化实体类代码和 MyBatisX 插件如果你用 MyBatis-Plus它能提供很好的 mapper 和 xml 跳转支持。1.2 前端环境Node.js、npm 与 Vue CLI前端我们使用 Vue 3Composition API或 Vue 2Options API都可以鉴于生态和资料Vue 2 可能更适合毕设。使用 Vue CLI 来创建和管理项目。1. Node.js去官网下载 LTS长期支持版本安装。安装后命令行输入node -v和npm -v检查版本。npm 是 Node.js 的包管理器。2. 配置 npm 镜像国内直接使用 npm 官方源很慢建议配置淘宝镜像。npm config set registry https://registry.npmmirror.com3. 安装 Vue CLIVue CLI 是一个全局的命令行工具。npm install -g vue/cli # 安装后验证 vue --version4. 浏览器插件在 Chrome 或 Edge 浏览器中安装 “Vue.js devtools” 插件。这是调试 Vue 应用的必备工具可以查看组件树、数据状态和事件。1.3 使用 Spring Initializr 快速创建后端项目不要在 IDEA 里手动建 Maven 项目再一个个加依赖直接用 Spring Initializr这是最稳妥的起点。打开 IDEA选择File - New - Project。左侧选择Spring Initializr。Project SDK选择你安装的 JDK 8 或 11。在初始化页面填写项目信息Group:com.redtourism(你的组织域名倒写)Artifact:server(后端项目名)Type:MavenLanguage:JavaPackaging:Jar(SpringBoot 推荐打成可执行 Jar)Java Version:8或11Version:保持默认点击Next进入依赖选择页面。这是关键一步我们勾选Spring Web(用于构建 Web 接口)MyBatis Framework或MyBatis-Plus(我强烈推荐 MyBatis-Plus它简化了大量CRUD代码)MySQL Driver(数据库驱动)Lombok(简化实体类代码必选)点击Next选择项目存放路径然后Finish。IDEA 会自动下载依赖并创建项目。创建完成后检查pom.xml文件确认上述依赖都已引入。1.4 创建前端 Vue 项目在后端项目同级目录下打开命令行运行vue create web这里web是你的前端项目文件夹名。Vue CLI 会交互式地让你选择配置Please pick a preset:选择Manually select features(手动选择特性)。Check the features needed for your project:用空格键勾选Babel,Router,Vuex,CSS Pre-processors,Linter / Formatter。Router 和 Vuex 对于管理页面路由和全局状态很有用。Choose a version of Vue.js:选择2.x。Use history mode for router?输入y。Pick a CSS pre-processor:选择Sass/SCSS (with node-sass)。Pick a linter / formatter config:选择ESLint Prettier。Pick additional lint features:选择Lint on save。Where do you prefer placing config for Babel, ESLint, etc.?选择In dedicated config files。Save this as a preset for future projects?输入n。等待创建完成。进入web目录运行npm run serve如果能在浏览器打开http://localhost:8080看到 Vue 欢迎页说明前端项目创建成功。至此你的工作区应该有两个文件夹server(SpringBoot后端) 和web(Vue前端)。环境准备完毕。2. 后端核心实现数据库设计、实体、Mapper 与 Service后端是系统的基石逻辑清晰、结构规范的后端代码是毕设答辩的加分项。2.1 数据库表设计以“红色景点”为例不要一上来就写代码先设计数据库。对于“红色旅游系统”核心实体至少包括用户、景点、门票订单、评论、新闻公告等。我们以scenic_spot(景点表) 为例CREATE TABLE scenic_spot ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 主键ID, name varchar(100) NOT NULL COMMENT 景点名称, description text COMMENT 景点描述, location varchar(255) COMMENT 具体位置, image_url varchar(500) COMMENT 封面图片URL, open_time varchar(100) COMMENT 开放时间, ticket_price decimal(10,2) COMMENT 门票价格, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, is_deleted tinyint(1) DEFAULT 0 COMMENT 逻辑删除标志(0未删1已删), PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT红色景点表;注意几点使用utf8mb4字符集支持存储 Emoji 等特殊字符。使用AUTO_INCREMENT作为主键自增。添加create_time,update_time记录数据变更这是好习惯。使用is_deleted实现逻辑删除而不是物理删除数据。字段注释 (COMMENT) 一定要写清楚这对后期维护和写文档至关重要。2.2 配置数据库连接与 MyBatis-Plus在server/src/main/resources/application.yml中配置数据库spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/red_tourism?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的密码 mybatis-plus: configuration: # 控制台打印 SQL 日志调试时非常有用 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: isDeleted # 全局逻辑删除字段名 logic-delete-value: 1 # 逻辑已删除值 logic-not-delete-value: 0 # 逻辑未删除值在pom.xml中添加 MyBatis-Plus 依赖如果 Initializr 没选dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency2.3 创建实体类、Mapper、Service 和 Controller这是标准的四层结构。MyBatis-Plus 能极大简化 Mapper 和 Service 的编写。实体类 (ScenicSpot)在com.redtourism.server.entity包下创建。package com.redtourism.server.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.math.BigDecimal; import java.util.Date; Data TableName(scenic_spot) // 指定表名 public class ScenicSpot { TableId(type IdType.AUTO) // 主键自增 private Integer id; private String name; private String description; private String location; private String imageUrl; private String openTime; private BigDecimal ticketPrice; TableField(fill FieldFill.INSERT) // 插入时自动填充 private Date createTime; TableField(fill FieldFill.INSERT_UPDATE) // 插入和更新时自动填充 private Date updateTime; TableLogic // 逻辑删除注解 private Integer isDeleted; }Data是 Lombok 注解自动生成 getter、setter、toString 等方法。Mapper 接口 (ScenicSpotMapper)在com.redtourism.server.mapper包下创建。package com.redtourism.server.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.redtourism.server.entity.ScenicSpot; public interface ScenicSpotMapper extends BaseMapperScenicSpot { // 继承 BaseMapper 后基础的 CRUD 方法都有了无需写 XML }Service 接口 (IScenicSpotService) 和实现类 (ScenicSpotServiceImpl):// IScenicSpotService.java package com.redtourism.server.service; import com.baomidou.mybatisplus.extension.service.IService; import com.redtourism.server.entity.ScenicSpot; public interface IScenicSpotService extends IServiceScenicSpot { // 可以在这里定义复杂的业务方法 } // ScenicSpotServiceImpl.java package com.redtourism.server.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.redtourism.server.entity.ScenicSpot; import com.redtourism.server.mapper.ScenicSpotMapper; import com.redtourism.server.service.IScenicSpotService; import org.springframework.stereotype.Service; Service public class ScenicSpotServiceImpl extends ServiceImplScenicSpotMapper, ScenicSpot implements IScenicSpotService { // 复杂的业务逻辑可以在这里实现 }Controller (ScenicSpotController)在com.redtourism.server.controller包下创建。这是提供 HTTP 接口的地方。package com.redtourism.server.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.redtourism.server.common.Result; import com.redtourism.server.entity.ScenicSpot; import com.redtourism.server.service.IScenicSpotService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/scenic-spot) public class ScenicSpotController { Autowired private IScenicSpotService scenicSpotService; // 新增景点 PostMapping public Result save(RequestBody ScenicSpot scenicSpot) { boolean success scenicSpotService.save(scenicSpot); return success ? Result.success() : Result.error(新增失败); } // 根据ID删除逻辑删除 DeleteMapping(/{id}) public Result delete(PathVariable Integer id) { boolean success scenicSpotService.removeById(id); return success ? Result.success() : Result.error(删除失败); } // 更新景点信息 PutMapping public Result update(RequestBody ScenicSpot scenicSpot) { boolean success scenicSpotService.updateById(scenicSpot); return success ? Result.success() : Result.error(更新失败); } // 根据ID查询单个景点 GetMapping(/{id}) public Result getById(PathVariable Integer id) { ScenicSpot spot scenicSpotService.getById(id); return Result.success(spot); } // 分页查询景点列表带条件 GetMapping(/page) public Result findPage(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String name) { LambdaQueryWrapperScenicSpot wrapper new LambdaQueryWrapper(); wrapper.like(name ! null !name.isEmpty(), ScenicSpot::getName, name); // 按名称模糊查询 wrapper.orderByDesc(ScenicSpot::getCreateTime); // 按创建时间倒序 IPageScenicSpot page new Page(pageNum, pageSize); IPageScenicSpot pageResult scenicSpotService.page(page, wrapper); return Result.success(pageResult); } }这里用到了一个统一的返回结果类Result你需要自己创建它在common包下用于规范接口返回格式。package com.redtourism.server.common; import lombok.Data; Data public class ResultT { private Integer code; // 状态码如 200成功500失败 private String msg; // 提示信息 private T data; // 返回的数据 public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMsg(操作成功); result.setData(data); return result; } public static T ResultT success() { return success(null); } public static T ResultT error(String msg) { ResultT result new Result(); result.setCode(500); result.setMsg(msg); return result; } }启动类与包扫描确保你的主启动类ServerApplication上有SpringBootApplication注解并且能扫描到上述所有包。通常放在根包com.redtourism.server下即可。现在启动你的 SpringBoot 应用。访问http://localhost:8080SpringBoot 默认端口可能会看到 Whitelabel Error Page这是正常的因为我们还没写前端页面。但你可以用 Postman 或浏览器直接测试接口例如GET http://localhost:8080/scenic-spot/page应该能看到返回的分页数据如果数据库里有数据的话。观察控制台MyBatis-Plus 会打印出执行的 SQL 语句。3. 前端核心实现Vue 组件、路由、状态管理与接口调用前端负责展示和交互结构清晰的前端代码能让你的演示更流畅。3.1 配置前端代理解决跨域问题在开发环境下前端运行在localhost:8080后端运行在localhost:8081假设你改了后端端口浏览器会因同源策略阻止请求。在 Vue 项目中配置代理是最简单的解决方案。在web项目根目录下找到或创建vue.config.js文件module.exports { devServer: { port: 8080, // 前端开发服务器端口 proxy: { /api: { // 将所有以 /api 开头的请求转发到后端 target: http://localhost:8081, // 你的后端地址 changeOrigin: true, // 改变请求头中的host为目标地址的host pathRewrite: { ^/api: // 将请求路径中的 /api 前缀去掉 } } } } }这样你在前端代码中请求/api/scenic-spot/page实际上会被转发到http://localhost:8081/scenic-spot/page。3.2 安装并配置 Axios 进行网络请求Vue 项目默认没有 HTTP 请求库我们安装最常用的 Axios。cd web npm install axios为了统一处理请求和响应我们通常创建一个request.js工具文件。在src目录下创建utils/request.jsimport axios from axios import { Message } from element-ui // 假设你用了 Element UI 的提示组件 import router from ../router // 创建 axios 实例 const service axios.create({ baseURL: /api, // 基础路径会与 vue.config.js 中的代理配置拼接 timeout: 10000 // 请求超时时间 }) // 请求拦截器 service.interceptors.request.use( config { // 在发送请求之前做些什么例如添加 token const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }, error { // 对请求错误做些什么 console.log(error) return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( response { const res response.data // 这里根据你后端 Result 的格式判断 if (res.code 200) { return res.data // 直接返回数据部分 } else { Message.error(res.msg || 请求失败) // 如果是未登录等状态码可以跳转到登录页 if (res.code 401) { router.push(/login) } return Promise.reject(new Error(res.msg || Error)) } }, error { console.log(err error) Message.error(网络错误或服务器异常) return Promise.reject(error) } ) export default service3.3 创建景点管理页面组件我们使用 Element UI 作为 UI 组件库它功能丰富适合快速搭建后台管理系统。npm install element-ui在src/main.js中引入import Vue from vue import ElementUI from element-ui import element-ui/lib/theme-chalk/index.css import App from ./App.vue import router from ./router import store from ./store Vue.use(ElementUI) new Vue({ router, store, render: h h(App) }).$mount(#app)现在创建景点列表页面。在src/views目录下创建ScenicSpot.vuetemplate div classscenic-spot-container el-card !-- 搜索和新增按钮 -- div stylemargin-bottom: 20px; el-input v-modelsearchForm.name placeholder请输入景点名称 stylewidth: 200px; keyup.enter.nativehandleSearch/el-input el-button typeprimary iconel-icon-search clickhandleSearch搜索/el-button el-button typesuccess iconel-icon-plus clickhandleAdd新增景点/el-button /div !-- 景点列表表格 -- el-table :datatableData border stripe stylewidth: 100% el-table-column propid labelID width80/el-table-column el-table-column propname label景点名称/el-table-column el-table-column proplocation label位置/el-table-column el-table-column propticketPrice label门票价格 width120 template slot-scopescope {{ scope.row.ticketPrice }} /template /el-table-column el-table-column propcreateTime label创建时间 width180/el-table-column el-table-column label操作 width200 fixedright template slot-scopescope el-button typetext sizesmall clickhandleEdit(scope.row)编辑/el-button el-button typetext sizesmall clickhandleDelete(scope.row.id) stylecolor: #f56c6c;删除/el-button /template /el-table-column /el-table !-- 分页组件 -- div stylemargin-top: 20px; text-align: center; el-pagination size-changehandleSizeChange current-changehandleCurrentChange :current-pagepageNum :page-sizes[5, 10, 20, 50] :page-sizepageSize layouttotal, sizes, prev, pager, next, jumper :totaltotal /el-pagination /div /el-card !-- 新增/编辑对话框 -- el-dialog :titledialogTitle :visible.syncdialogVisible width40% el-form :modelform :rulesrules refformRef label-width80px el-form-item label景点名称 propname el-input v-modelform.name/el-input /el-form-item el-form-item label景点描述 propdescription el-input typetextarea v-modelform.description :rows3/el-input /el-form-item el-form-item label具体位置 proplocation el-input v-modelform.location/el-input /el-form-item el-form-item label门票价格 propticketPrice el-input v-modelform.ticketPrice typenumber min0/el-input /el-form-item el-form-item label开放时间 propopenTime el-input v-modelform.openTime/el-input /el-form-item el-form-item label封面图片 propimageUrl el-input v-modelform.imageUrl placeholder请输入图片URL/el-input !-- 实际项目中这里应该是图片上传组件 -- /el-form-item /el-form span slotfooter classdialog-footer el-button clickdialogVisible false取 消/el-button el-button typeprimary clicksubmitForm确 定/el-button /span /el-dialog /div /template script import { getScenicSpotPage, saveScenicSpot, updateScenicSpot, deleteScenicSpot } from /api/scenicSpot export default { name: ScenicSpot, data() { return { tableData: [], searchForm: { name: }, pageNum: 1, pageSize: 10, total: 0, dialogVisible: false, dialogTitle: 新增景点, form: { id: null, name: , description: , location: , ticketPrice: 0, openTime: , imageUrl: }, rules: { name: [{ required: true, message: 请输入景点名称, trigger: blur }], location: [{ required: true, message: 请输入具体位置, trigger: blur }] } } }, created() { this.fetchData() }, methods: { // 获取分页数据 async fetchData() { const params { pageNum: this.pageNum, pageSize: this.pageSize, name: this.searchForm.name } try { const res await getScenicSpotPage(params) this.tableData res.records this.total res.total } catch (error) { console.error(获取数据失败:, error) } }, // 搜索 handleSearch() { this.pageNum 1 this.fetchData() }, // 新增 handleAdd() { this.dialogTitle 新增景点 this.form { id: null, name: , description: , location: , ticketPrice: 0, openTime: , imageUrl: } this.dialogVisible true this.$nextTick(() { this.$refs[formRef]?.clearValidate() }) }, // 编辑 handleEdit(row) { this.dialogTitle 编辑景点 this.form { ...row } // 浅拷贝避免直接修改表格数据 this.dialogVisible true this.$nextTick(() { this.$refs[formRef]?.clearValidate() }) }, // 提交表单新增或更新 submitForm() { this.$refs[formRef].validate(async (valid) { if (valid) { try { if (this.form.id) { // 更新 await updateScenicSpot(this.form) this.$message.success(更新成功) } else { // 新增 await saveScenicSpot(this.form) this.$message.success(新增成功) } this.dialogVisible false this.fetchData() // 刷新列表 } catch (error) { this.$message.error(操作失败) } } }) }, // 删除 async handleDelete(id) { try { await this.$confirm(确定删除该景点吗, 提示, { type: warning }) await deleteScenicSpot(id) this.$message.success(删除成功) this.fetchData() } catch (error) { if (error ! cancel) { this.$message.error(删除失败) } } }, // 分页大小改变 handleSizeChange(val) { this.pageSize val this.pageNum 1 this.fetchData() }, // 当前页改变 handleCurrentChange(val) { this.pageNum val this.fetchData() } } } /script style scoped .scenic-spot-container { padding: 20px; } /style3.4 创建 API 层封装在src目录下创建api文件夹然后创建scenicSpot.jsimport request from /utils/request // 分页查询景点列表 export function getScenicSpotPage(params) { return request({ url: /scenic-spot/page, method: get, params }) } // 新增景点 export function saveScenicSpot(data) { return request({ url: /scenic-spot, method: post, data }) } // 更新景点 export function updateScenicSpot(data) { return request({ url: /scenic-spot, method: put, data }) } // 删除景点 export function deleteScenicSpot(id) { return request({ url: /scenic-spot/${id}, method: delete }) }3.5 配置路由在src/router/index.js中添加景点管理页面的路由import Vue from vue import VueRouter from vue-router import ScenicSpot from /views/ScenicSpot.vue Vue.use(VueRouter) const routes [ // ... 其他路由 { path: /scenic-spot, name: ScenicSpot, component: ScenicSpot } ] const router new VueRouter({ mode: history, base: process.env.BASE_URL, routes }) export default router现在启动前端 (npm run serve) 和后端访问http://localhost:8080/#/scenic-spot如果你的路由模式是 hash应该能看到一个具备增删改查、分页搜索功能的景点管理页面。尝试新增一条数据观察浏览器网络请求和控制台日志理解前后端数据是如何交互的。4. 系统扩展与毕设亮点打造完成基础的 CRUD 只是第一步。要让你的毕设脱颖而出需要在业务逻辑、系统设计和用户体验上增加亮点。4.1 实现用户认证与权限管理一个完整的系统必须有用户体系。实现 JWT (JSON Web Token) 认证是常见且标准的做法。后端实现添加spring-boot-starter-security和jjwt依赖。创建User实体、Mapper、Service。创建AuthController提供/login接口验证用户名密码后生成 JWT Token 返回。创建JwtAuthenticationFilter过滤器在每次请求前验证 Token。创建SecurityConfig配置类放行登录接口保护其他接口。在需要权限的接口上使用PreAuthorize(hasRole(ADMIN))等注解。前端实现创建登录页面。登录成功后将后端返回的 Token 存入localStorage或Vuex。在request.js的请求拦截器中将 Token 添加到请求头Authorization中。实现路由守卫 (router.beforeEach)检查用户是否登录未登录则跳转到登录页。根据用户角色动态生成侧边栏菜单需要后端接口返回用户权限菜单列表。4.2 实现图片上传功能景点需要封面图。不要只存一个 URL 字段实现一个本地上传功能。后端实现在application.yml中配置文件上传大小限制和存储路径。spring: servlet: multipart: max-file-size: 10MB max-request-size: 100MB创建FileController提供一个POST /upload接口使用MultipartFile接收文件。将文件保存到服务器指定目录如uploads/并生成一个可访问的 URL如http://your-server:port/uploads/filename.jpg。需要考虑文件名重复、文件类型校验、目录不存在自动创建等问题。更优方案是集成对象存储服务如阿里云 OSS、腾讯云 COS但本地存储对于毕设演示足够。前端实现使用 Element UI 的el-upload组件。在上传成功的回调中将后端返回的文件 URL 赋值给表单的imageUrl字段。在表格中使用el-image组件预览图片。4.3 引入 Redis 缓存提升性能对于热点数据如首页推荐景点、公告可以引入 Redis 缓存减少数据库压力。添加依赖spring-boot-starter-data-redis。配置 Redis 连接信息。在 Service 层使用缓存在查询方法上添加Cacheable注解在更新/删除方法上添加CacheEvict注解。Service public class ScenicSpotServiceImpl ... { Cacheable(value scenicSpot, key #id) public ScenicSpot getByIdWithCache(Integer id) { return getById(id); } CacheEvict(value scenicSpot, key #scenicSpot.id) public boolean updateWithCache(ScenicSpot scenicSpot) { return updateById(scenicSpot); } }4.4 实现更复杂的业务门票预订与订单管理这是体现你业务建模能力的好地方。数据库设计创建ticket_order表关联user_id和scenic_spot_id包含订单号、购买数量、总价、订单状态待支付、已支付、已取消、已完成、创建时间等字段。后端实现创建TicketOrder实体、Mapper、Service、Controller。实现创建订单检查库存、生成唯一订单号、支付回调更新订单状态、取消订单等接口。考虑并发问题多人同时预订同一景点的最后几张票。可以使用数据库乐观锁版本号或 Redis 分布式锁来保证数据一致性。前端实现在景点详情页增加“立即预订”按钮。创建订单确认页和订单列表页。集成模拟支付流程例如点击支付按钮后直接修改订单状态为“已支付”。4.5 部署与演示准备最终你需要将项目部署到服务器或本地进行演示。后端打包在server目录下执行mvn clean package会在target目录生成一个server-0.0.1-SNAPSHOT.jar文件。前端打包在web目录下执行npm run build会在dist目录生成静态文件。部署方式一前后端分离将jar包上传到服务器用java -jar server-0.0.1-SNAPSHOT.jar启动后端。将dist文件夹里的内容放到 Nginx 或 Apache 的静态资源目录下。配置 Nginx 反向代理将/api请求转发到后端服务并处理前端路由的 History 模式问题。部署方式二前后端合并适合本地演示将前端打包后的dist文件夹内容复制到后端项目的src/main/resources/static目录下。重新打包 SpringBoot 项目这个 Jar 包就包含了前端静态资源。运行这个 Jar 包访问http://localhost:8080就能看到完整应用。这种方式最简单适合毕设答辩现场演示。给毕设答辩的建议准备演讲稿和演示流程不要现场临时发挥。先讲项目背景和意义红色旅游、文化传承再讲技术选型为什么用 SpringBootVue然后演示核心功能登录、景点管理、门票预订最后展示代码结构和技术亮点JWT、Redis、文件上传、权限控制。准备好数据库脚本和演示数据确保评委老师能一键导入数据看到完整效果。重点讲解 1-2 个复杂业务或技术难点比如“如何防止超卖”、“JWT 认证流程”、“Redis 缓存设计与更新策略”。这能体现你的深度。代码规范确保你的代码有清晰的注释、合理的包结构、规范的命名。这是隐形的加分项。这个项目骨架已经为你搭好沿着这个路径补充用户管理、订单管理、评论系统、新闻公告、数据统计图表等模块一个丰满的“红色革命老区旅游系统”就完成了。记住毕设的核心是展示你运用技术解决实际问题的能力把基础做扎实把一两个亮点做深入远比堆砌一堆半成品功能要强。