SpringBoot+Vue前后端分离项目实战:从零搭建红色旅游系统
2026/7/23 5:47:18 网站建设 项目流程

这类毕业设计项目最值得关注的不是功能有多全,而是能不能把前后端技术栈跑通、把核心业务流程走顺,并且能清晰地向导师展示你的技术选型、架构设计和实现细节。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 -versionjavac -version确认版本。

2. Maven:用于管理项目依赖和打包。去官网下载,解压后配置环境变量MAVEN_HOMEPATH。在命令行输入mvn -v能显示版本信息即表示成功。我建议在~/.m2/settings.xml文件中配置阿里云的镜像仓库,能大幅加快依赖下载速度。

<mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>

3. MySQL:安装 MySQL 8.0,记住你设置的 root 密码。安装完成后,创建一个专门用于本项目的数据库,比如red_tourism,字符集用utf8mb4,排序规则用utf8mb4_general_ci

4. IDEA(IntelliJ IDEA):这是开发 SpringBoot 项目最主流的 IDE。确保安装了 Lombok 插件(用于简化实体类代码)和 MyBatisX 插件(如果你用 MyBatis-Plus,它能提供很好的 mapper 和 xml 跳转支持)。

1.2 前端环境:Node.js、npm 与 Vue CLI

前端我们使用 Vue 3(Composition API)或 Vue 2(Options API)都可以,鉴于生态和资料,Vue 2 可能更适合毕设。使用 Vue CLI 来创建和管理项目。

1. Node.js:去官网下载 LTS(长期支持)版本安装。安装后,命令行输入node -vnpm -v检查版本。npm 是 Node.js 的包管理器。

2. 配置 npm 镜像:国内直接使用 npm 官方源很慢,建议配置淘宝镜像。

npm config set registry https://registry.npmmirror.com

3. 安装 Vue CLI:Vue CLI 是一个全局的命令行工具。

npm install -g @vue/cli # 安装后验证 vue --version

4. 浏览器插件:在 Chrome 或 Edge 浏览器中安装 “Vue.js devtools” 插件。这是调试 Vue 应用的必备工具,可以查看组件树、数据状态和事件。

1.3 使用 Spring Initializr 快速创建后端项目

不要在 IDEA 里手动建 Maven 项目再一个个加依赖,直接用 Spring Initializr,这是最稳妥的起点。

  1. 打开 IDEA,选择File -> New -> Project
  2. 左侧选择Spring Initializr
  3. Project SDK选择你安装的 JDK 8 或 11。
  4. 在初始化页面,填写项目信息:
    • Group:com.redtourism(你的组织域名倒写)
    • Artifact:server(后端项目名)
    • Type:Maven
    • Language:Java
    • Packaging:Jar(SpringBoot 推荐打成可执行 Jar)
    • Java Version:811
    • Version:保持默认
  5. 点击Next,进入依赖选择页面。这是关键一步,我们勾选:
    • Spring Web(用于构建 Web 接口)
    • MyBatis FrameworkMyBatis-Plus(我强烈推荐 MyBatis-Plus,它简化了大量CRUD代码)
    • MySQL Driver(数据库驱动)
    • Lombok(简化实体类代码,必选)
  6. 点击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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='红色景点表';

注意几点:

  1. 使用utf8mb4字符集支持存储 Emoji 等特殊字符。
  2. 使用AUTO_INCREMENT作为主键自增。
  3. 添加create_time,update_time记录数据变更,这是好习惯。
  4. 使用is_deleted实现逻辑删除,而不是物理删除数据。
  5. 字段注释 (COMMENT) 一定要写清楚,这对后期维护和写文档至关重要。

2.2 配置数据库连接与 MyBatis-Plus

  1. server/src/main/resources/application.yml中配置数据库:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/red_tourism?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/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 # 逻辑未删除值
  1. pom.xml中添加 MyBatis-Plus 依赖(如果 Initializr 没选):
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency>

2.3 创建实体类、Mapper、Service 和 Controller

这是标准的四层结构。MyBatis-Plus 能极大简化 Mapper 和 Service 的编写。

  1. 实体类 (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 等方法。

  1. 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 BaseMapper<ScenicSpot> { // 继承 BaseMapper 后,基础的 CRUD 方法都有了,无需写 XML }
  1. 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 IService<ScenicSpot> { // 可以在这里定义复杂的业务方法 } // 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 ServiceImpl<ScenicSpotMapper, ScenicSpot> implements IScenicSpotService { // 复杂的业务逻辑可以在这里实现 }
  1. 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) { LambdaQueryWrapper<ScenicSpot> wrapper = new LambdaQueryWrapper<>(); wrapper.like(name != null && !name.isEmpty(), ScenicSpot::getName, name); // 按名称模糊查询 wrapper.orderByDesc(ScenicSpot::getCreateTime); // 按创建时间倒序 IPage<ScenicSpot> page = new Page<>(pageNum, pageSize); IPage<ScenicSpot> pageResult = scenicSpotService.page(page, wrapper); return Result.success(pageResult); } }

这里用到了一个统一的返回结果类Result,你需要自己创建它(在common包下),用于规范接口返回格式。

package com.redtourism.server.common; import lombok.Data; @Data public class Result<T> { private Integer code; // 状态码,如 200成功,500失败 private String msg; // 提示信息 private T data; // 返回的数据 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMsg("操作成功"); result.setData(data); return result; } public static <T> Result<T> success() { return success(null); } public static <T> Result<T> error(String msg) { Result<T> result = new Result<>(); result.setCode(500); result.setMsg(msg); return result; } }
  1. 启动类与包扫描:确保你的主启动类ServerApplication上有@SpringBootApplication注解,并且能扫描到上述所有包。通常放在根包com.redtourism.server下即可。

现在,启动你的 SpringBoot 应用。访问http://localhost:8080(SpringBoot 默认端口)可能会看到 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.js

import 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 service

3.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.vue

<template> <div class="scenic-spot-container"> <el-card> <!-- 搜索和新增按钮 --> <div style="margin-bottom: 20px;"> <el-input v-model="searchForm.name" placeholder="请输入景点名称" style="width: 200px;" @keyup.enter.native="handleSearch"></el-input> <el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button> <el-button type="success" icon="el-icon-plus" @click="handleAdd">新增景点</el-button> </div> <!-- 景点列表表格 --> <el-table :data="tableData" border stripe style="width: 100%"> <el-table-column prop="id" label="ID" width="80"></el-table-column> <el-table-column prop="name" label="景点名称"></el-table-column> <el-table-column prop="location" label="位置"></el-table-column> <el-table-column prop="ticketPrice" label="门票价格" width="120"> <template slot-scope="scope"> ¥{{ scope.row.ticketPrice }} </template> </el-table-column> <el-table-column prop="createTime" label="创建时间" width="180"></el-table-column> <el-table-column label="操作" width="200" fixed="right"> <template slot-scope="scope"> <el-button type="text" size="small" @click="handleEdit(scope.row)">编辑</el-button> <el-button type="text" size="small" @click="handleDelete(scope.row.id)" style="color: #f56c6c;">删除</el-button> </template> </el-table-column> </el-table> <!-- 分页组件 --> <div style="margin-top: 20px; text-align: center;"> <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="pageNum" :page-sizes="[5, 10, 20, 50]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper" :total="total"> </el-pagination> </div> </el-card> <!-- 新增/编辑对话框 --> <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="40%"> <el-form :model="form" :rules="rules" ref="formRef" label-width="80px"> <el-form-item label="景点名称" prop="name"> <el-input v-model="form.name"></el-input> </el-form-item> <el-form-item label="景点描述" prop="description"> <el-input type="textarea" v-model="form.description" :rows="3"></el-input> </el-form-item> <el-form-item label="具体位置" prop="location"> <el-input v-model="form.location"></el-input> </el-form-item> <el-form-item label="门票价格" prop="ticketPrice"> <el-input v-model="form.ticketPrice" type="number" min="0"></el-input> </el-form-item> <el-form-item label="开放时间" prop="openTime"> <el-input v-model="form.openTime"></el-input> </el-form-item> <el-form-item label="封面图片" prop="imageUrl"> <el-input v-model="form.imageUrl" placeholder="请输入图片URL"></el-input> <!-- 实际项目中,这里应该是图片上传组件 --> </el-form-item> </el-form> <span slot="footer" class="dialog-footer"> <el-button @click="dialogVisible = false">取 消</el-button> <el-button type="primary" @click="submitForm">确 定</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; } </style>

3.4 创建 API 层封装

src目录下创建api文件夹,然后创建scenicSpot.js

import 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) 认证是常见且标准的做法。

  1. 后端实现:

    • 添加spring-boot-starter-securityjjwt依赖。
    • 创建User实体、Mapper、Service。
    • 创建AuthController,提供/login接口,验证用户名密码后生成 JWT Token 返回。
    • 创建JwtAuthenticationFilter过滤器,在每次请求前验证 Token。
    • 创建SecurityConfig配置类,放行登录接口,保护其他接口。
    • 在需要权限的接口上使用@PreAuthorize("hasRole('ADMIN')")等注解。
  2. 前端实现:

    • 创建登录页面。
    • 登录成功后,将后端返回的 Token 存入localStorageVuex
    • request.js的请求拦截器中,将 Token 添加到请求头Authorization中。
    • 实现路由守卫 (router.beforeEach),检查用户是否登录,未登录则跳转到登录页。
    • 根据用户角色,动态生成侧边栏菜单(需要后端接口返回用户权限菜单列表)。

4.2 实现图片上传功能

景点需要封面图。不要只存一个 URL 字段,实现一个本地上传功能。

  1. 后端实现:

    • 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),但本地存储对于毕设演示足够。
  2. 前端实现:

    • 使用 Element UI 的el-upload组件。
    • 在上传成功的回调中,将后端返回的文件 URL 赋值给表单的imageUrl字段。
    • 在表格中,使用<el-image>组件预览图片。

4.3 引入 Redis 缓存提升性能

对于热点数据(如首页推荐景点、公告),可以引入 Redis 缓存,减少数据库压力。

  1. 添加依赖:spring-boot-starter-data-redis
  2. 配置 Redis 连接信息。
  3. 在 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 实现更复杂的业务:门票预订与订单管理

这是体现你业务建模能力的好地方。

  1. 数据库设计:创建ticket_order表,关联user_idscenic_spot_id,包含订单号、购买数量、总价、订单状态(待支付、已支付、已取消、已完成)、创建时间等字段。
  2. 后端实现:
    • 创建TicketOrder实体、Mapper、Service、Controller。
    • 实现创建订单(检查库存、生成唯一订单号)、支付回调(更新订单状态)、取消订单等接口。
    • 考虑并发问题:多人同时预订同一景点的最后几张票。可以使用数据库乐观锁(版本号)或 Redis 分布式锁来保证数据一致性。
  3. 前端实现:
    • 在景点详情页,增加“立即预订”按钮。
    • 创建订单确认页和订单列表页。
    • 集成模拟支付流程(例如,点击支付按钮后,直接修改订单状态为“已支付”)。

4.5 部署与演示准备

最终你需要将项目部署到服务器或本地,进行演示。

  1. 后端打包:server目录下执行mvn clean package,会在target目录生成一个server-0.0.1-SNAPSHOT.jar文件。
  2. 前端打包:web目录下执行npm run build,会在dist目录生成静态文件。
  3. 部署方式一(前后端分离):
    • jar包上传到服务器,用java -jar server-0.0.1-SNAPSHOT.jar启动后端。
    • dist文件夹里的内容放到 Nginx 或 Apache 的静态资源目录下。
    • 配置 Nginx 反向代理,将/api请求转发到后端服务,并处理前端路由的 History 模式问题。
  4. 部署方式二(前后端合并,适合本地演示):
    • 将前端打包后的dist文件夹内容,复制到后端项目的src/main/resources/static目录下。
    • 重新打包 SpringBoot 项目,这个 Jar 包就包含了前端静态资源。
    • 运行这个 Jar 包,访问http://localhost:8080就能看到完整应用。这种方式最简单,适合毕设答辩现场演示。

给毕设答辩的建议:

  1. 准备演讲稿和演示流程:不要现场临时发挥。先讲项目背景和意义(红色旅游、文化传承),再讲技术选型(为什么用 SpringBoot+Vue),然后演示核心功能(登录、景点管理、门票预订),最后展示代码结构和技术亮点(JWT、Redis、文件上传、权限控制)。
  2. 准备好数据库脚本和演示数据:确保评委老师能一键导入数据,看到完整效果。
  3. 重点讲解 1-2 个复杂业务或技术难点:比如“如何防止超卖”、“JWT 认证流程”、“Redis 缓存设计与更新策略”。这能体现你的深度。
  4. 代码规范:确保你的代码有清晰的注释、合理的包结构、规范的命名。这是隐形的加分项。

这个项目骨架已经为你搭好,沿着这个路径,补充用户管理、订单管理、评论系统、新闻公告、数据统计图表等模块,一个丰满的“红色革命老区旅游系统”就完成了。记住,毕设的核心是展示你运用技术解决实际问题的能力,把基础做扎实,把一两个亮点做深入,远比堆砌一堆半成品功能要强。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询