SpringBoot+Vue旅游信息管理系统开发实践
发布时间:2026/9/25 7:20:12来源:尧图网络
1. 项目概述作为一名长期从事旅游信息化系统开发的工程师我最近完成了一个基于SpringBootVue的旅游信息管理系统项目。这个系统从最初的需求调研到最终上线运行前后历时6个月期间踩过不少坑也积累了不少经验。今天我就把这个项目的完整开发过程分享给大家希望能给正在开发类似系统的同行一些参考。这个系统主要解决旅游行业中的三个核心痛点旅游信息分散游客需要到多个平台查询景点、酒店、路线等信息预订流程繁琐往往需要反复切换不同系统管理端功能薄弱景区和酒店难以高效管理自己的资源系统采用前后端分离架构后端使用SpringBoot 2.7.3 MyBatis Plus MySQL 8.0前端使用Vue 3 Element Plus Axios。这种技术组合在保证系统性能的同时也大大提升了开发效率。下面我就从系统设计到实现细节一步步为大家解析这个项目的完整开发过程。2. 技术选型与架构设计2.1 后端技术栈选择选择SpringBoot作为后端框架主要基于以下几个考虑自动配置特性大幅减少了XML配置快速启动项目内嵌Tomcat服务器简化部署流程丰富的Starter依赖轻松集成各种组件完善的生态和社区支持数据库选用MySQL 8.0主要因为开源免费适合中小型项目事务支持完善保证数据一致性JSON数据类型支持便于存储景点、酒店等复杂结构数据良好的性能表现经测试单表千万级数据查询响应时间100ms2.2 前端技术栈选择Vue 3作为前端框架的优势组合式API更利于代码组织和复用更好的TypeScript支持更小的打包体积和更快的渲染速度完善的生态系统Vue Router、Pinia等Element Plus组件库的选择理由丰富的UI组件覆盖大部分业务场景良好的文档和示例活跃的社区支持与Vue 3完美兼容2.3 系统架构设计系统采用典型的三层架构表现层Vue 3 Element Plus业务逻辑层SpringBoot MyBatis Plus数据访问层MySQL 8.0前后端通过RESTful API交互使用JWT进行身份认证。这种架构的优势在于前后端完全解耦可独立开发和部署接口定义清晰便于团队协作易于扩展和维护3. 数据库设计与实现3.1 核心表结构设计系统主要包含以下几类核心表用户相关表CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码, phone varchar(20) COMMENT 手机号, email varchar(100) COMMENT 邮箱, avatar varchar(255) COMMENT 头像, status tinyint DEFAULT 1 COMMENT 状态 0:禁用 1:正常, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY idx_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表;景点相关表CREATE TABLE scenic_spot ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 景点名称, location varchar(255) NOT NULL COMMENT 地理位置, description text COMMENT 景点描述, cover_image varchar(255) COMMENT 封面图片, open_time varchar(100) COMMENT 开放时间, ticket_price decimal(10,2) COMMENT 门票价格, rating decimal(3,1) DEFAULT 0.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, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT景点信息表;酒店相关表CREATE TABLE hotel ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 酒店名称, location varchar(255) NOT NULL COMMENT 地理位置, description text COMMENT 酒店描述, cover_image varchar(255) COMMENT 封面图片, star_level tinyint COMMENT 星级, min_price decimal(10,2) COMMENT 最低价格, contact_phone varchar(20) COMMENT 联系电话, status tinyint DEFAULT 1 COMMENT 状态 0:下架 1:上架, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT酒店信息表;3.2 数据库优化实践在实际开发中我们针对查询性能做了以下优化合理设计索引-- 为景点表添加地理位置和名称的联合索引 ALTER TABLE scenic_spot ADD INDEX idx_location_name (location, name); -- 为酒店表添加星级和价格的联合索引 ALTER TABLE hotel ADD INDEX idx_star_price (star_level, min_price);使用分表策略处理大数据量用户评论表按月分表订单表按用户ID哈希分表配置MySQL参数优化# my.cnf配置 innodb_buffer_pool_size 4G innodb_log_file_size 512M innodb_flush_log_at_trx_commit 2 innodb_read_io_threads 8 innodb_write_io_threads 84. 后端核心功能实现4.1 用户认证模块采用JWT实现无状态认证核心代码如下// JWT工具类 public class JwtUtils { private static final String SECRET your-secret-key; private static final long EXPIRATION 86400L; // 24小时 public static String generateToken(UserDetails userDetails) { MapString, Object claims new HashMap(); claims.put(sub, userDetails.getUsername()); claims.put(created, new Date()); return Jwts.builder() .setClaims(claims) .setExpiration(new Date(System.currentTimeMillis() EXPIRATION * 1000)) .signWith(SignatureAlgorithm.HS512, SECRET) .compact(); } public static String getUsernameFromToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody() .getSubject(); } public static boolean validateToken(String token, UserDetails userDetails) { final String username getUsernameFromToken(token); return (username.equals(userDetails.getUsername()) !isTokenExpired(token)); } private static boolean isTokenExpired(String token) { final Date expiration getExpirationDateFromToken(token); return expiration.before(new Date()); } }4.2 景点管理模块实现景点CRUD和搜索功能RestController RequestMapping(/api/scenic) public class ScenicSpotController { Autowired private ScenicSpotService scenicSpotService; GetMapping public ResponseEntityPageResultScenicSpot listScenicSpots( RequestParam(required false) String keyword, RequestParam(required false) String location, RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { PageScenicSpot scenicSpots scenicSpotService.searchScenicSpots(keyword, location, page, size); PageResultScenicSpot result new PageResult( scenicSpots.getTotalElements(), scenicSpots.getContent()); return ResponseEntity.ok(result); } PostMapping public ResponseEntityScenicSpot createScenicSpot(Valid RequestBody ScenicSpotDTO scenicSpotDTO) { ScenicSpot scenicSpot scenicSpotService.createScenicSpot(scenicSpotDTO); return ResponseEntity.status(HttpStatus.CREATED).body(scenicSpot); } PutMapping(/{id}) public ResponseEntityScenicSpot updateScenicSpot( PathVariable Long id, Valid RequestBody ScenicSpotDTO scenicSpotDTO) { ScenicSpot scenicSpot scenicSpotService.updateScenicSpot(id, scenicSpotDTO); return ResponseEntity.ok(scenicSpot); } DeleteMapping(/{id}) public ResponseEntityVoid deleteScenicSpot(PathVariable Long id) { scenicSpotService.deleteScenicSpot(id); return ResponseEntity.noContent().build(); } }4.3 订单处理模块处理景点门票和酒店预订Service Transactional public class OrderServiceImpl implements OrderService { Autowired private ScenicSpotRepository scenicSpotRepository; Autowired private HotelRepository hotelRepository; Autowired private OrderRepository orderRepository; Override public Order createScenicOrder(Long userId, ScenicOrderDTO orderDTO) { ScenicSpot scenicSpot scenicSpotRepository.findById(orderDTO.getScenicId()) .orElseThrow(() - new ResourceNotFoundException(景点不存在)); if (scenicSpot.getStatus() ! 1) { throw new BusinessException(该景点暂不可预订); } Order order new Order(); order.setUserId(userId); order.setOrderType(OrderType.SCENIC); order.setItemId(scenicSpot.getId()); order.setItemName(scenicSpot.getName()); order.setQuantity(orderDTO.getQuantity()); order.setUnitPrice(scenicSpot.getTicketPrice()); order.setTotalAmount(scenicSpot.getTicketPrice().multiply(BigDecimal.valueOf(orderDTO.getQuantity()))); order.setStatus(OrderStatus.PENDING); order.setVisitDate(orderDTO.getVisitDate()); return orderRepository.save(order); } Override public Order createHotelOrder(Long userId, HotelOrderDTO orderDTO) { Hotel hotel hotelRepository.findById(orderDTO.getHotelId()) .orElseThrow(() - new ResourceNotFoundException(酒店不存在)); if (hotel.getStatus() ! 1) { throw new BusinessException(该酒店暂不可预订); } // 检查房间库存 // ... Order order new Order(); order.setUserId(userId); order.setOrderType(OrderType.HOTEL); order.setItemId(hotel.getId()); order.setItemName(hotel.getName()); order.setQuantity(orderDTO.getRoomCount()); order.setUnitPrice(orderDTO.getRoomPrice()); order.setTotalAmount(orderDTO.getRoomPrice().multiply(BigDecimal.valueOf(orderDTO.getRoomCount()))); order.setStatus(OrderStatus.PENDING); order.setCheckInDate(orderDTO.getCheckInDate()); order.setCheckOutDate(orderDTO.getCheckOutDate()); return orderRepository.save(order); } }5. 前端核心功能实现5.1 前端项目结构src/ ├── api/ # API请求封装 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── composables/ # 组合式函数 ├── router/ # 路由配置 ├── stores/ # Pinia状态管理 ├── styles/ # 全局样式 ├── utils/ # 工具函数 ├── views/ # 页面组件 │ ├── admin/ # 管理后台页面 │ ├── auth/ # 认证相关页面 │ ├── scenic/ # 景点相关页面 │ ├── hotel/ # 酒店相关页面 │ └── user/ # 用户中心页面 └── App.vue # 根组件5.2 景点列表页实现script setup import { ref, onMounted } from vue import { useScenicStore } from /stores/scenic import ScenicCard from /components/ScenicCard.vue const scenicStore useScenicStore() const loading ref(false) const searchParams ref({ keyword: , location: , page: 1, size: 10 }) const fetchScenicSpots async () { try { loading.value true await scenicStore.fetchScenicSpots(searchParams.value) } finally { loading.value false } } onMounted(() { fetchScenicSpots() }) /script template div classscenic-list-container el-row :gutter20 el-col :span6 div classfilter-panel el-input v-modelsearchParams.keyword placeholder搜索景点名称 clearable clearfetchScenicSpots keyup.enterfetchScenicSpots template #append el-button :iconSearch clickfetchScenicSpots / /template /el-input el-select v-modelsearchParams.location placeholder选择地区 clearable changefetchScenicSpots el-option v-foritem in locationOptions :keyitem.value :labelitem.label :valueitem.value / /el-select /div /el-col el-col :span18 div v-loadingloading classscenic-list template v-ifscenicStore.list.length 0 el-row :gutter20 el-col v-forscenic in scenicStore.list :keyscenic.id :xs24 :sm12 :md8 :lg6 scenic-card :scenicscenic / /el-col /el-row el-pagination v-model:current-pagesearchParams.page :page-sizesearchParams.size :totalscenicStore.total layouttotal, prev, pager, next, jumper current-changefetchScenicSpots / /template el-empty v-else description暂无景点数据 / /div /el-col /el-row /div /template style scoped .scenic-list-container { padding: 20px; } .filter-panel { position: sticky; top: 20px; display: flex; flex-direction: column; gap: 15px; padding: 15px; background-color: #fff; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .scenic-list { min-height: 500px; padding: 15px; background-color: #fff; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } /style5.3 订单支付流程实现script setup import { ref, onMounted } from vue import { useRouter } from vue-router import { useOrderStore } from /stores/order import { ElMessage } from element-plus const router useRouter() const orderStore useOrderStore() const loading ref(false) const paymentMethod ref(alipay) const handlePayment async () { try { loading.value true const paymentResult await orderStore.createPayment({ orderId: orderStore.currentOrder.id, method: paymentMethod.value }) if (paymentResult.payUrl) { window.open(paymentResult.payUrl, _blank) } else { ElMessage.success(支付成功) router.push(/user/orders) } } catch (error) { ElMessage.error(error.message || 支付失败) } finally { loading.value false } } onMounted(async () { if (!orderStore.currentOrder) { router.push(/) return } }) /script template div classpayment-container el-card template #header div classcard-header span订单支付/span /div /template el-descriptions :column1 border el-descriptions-item label订单编号 {{ orderStore.currentOrder?.orderNo }} /el-descriptions-item el-descriptions-item label商品名称 {{ orderStore.currentOrder?.itemName }} /el-descriptions-item el-descriptions-item label订单金额 ¥{{ orderStore.currentOrder?.totalAmount.toFixed(2) }} /el-descriptions-item /el-descriptions div classpayment-methods h4选择支付方式/h4 el-radio-group v-modelpaymentMethod el-radio labelalipay img src/assets/images/alipay.png alt支付宝 classpayment-icon span支付宝/span /el-radio el-radio labelwechat img src/assets/images/wechat.png alt微信支付 classpayment-icon span微信支付/span /el-radio /el-radio-group /div div classpayment-actions el-button typeprimary sizelarge :loadingloading clickhandlePayment 立即支付 /el-button el-button sizelarge clickrouter.push(/user/orders) 返回订单列表 /el-button /div /el-card /div /template style scoped .payment-container { max-width: 800px; margin: 0 auto; padding: 20px; } .card-header { font-size: 18px; font-weight: bold; } .payment-methods { margin: 30px 0; } .payment-methods h4 { margin-bottom: 15px; font-size: 16px; color: #666; } .payment-icon { width: 24px; height: 24px; vertical-align: middle; margin-right: 8px; } .payment-actions { display: flex; justify-content: center; gap: 20px; margin-top: 30px; } /style6. 系统部署与运维6.1 后端部署方案我们采用Docker容器化部署后端服务Dockerfile配置如下# 基础镜像 FROM openjdk:17-jdk-slim # 设置工作目录 WORKDIR /app # 复制构建好的jar包 COPY target/tourism-system-*.jar app.jar # 暴露端口 EXPOSE 8080 # 启动命令 ENTRYPOINT [java, -jar, app.jar]使用docker-compose编排服务version: 3.8 services: app: build: . ports: - 8080:8080 environment: - SPRING_PROFILES_ACTIVEprod - DB_URLjdbc:mysql://mysql:3306/tourism_db - DB_USERNAMEroot - DB_PASSWORDyourpassword depends_on: - mysql - redis mysql: image: mysql:8.0 ports: - 3306:3306 environment: - MYSQL_ROOT_PASSWORDyourpassword - MYSQL_DATABASEtourism_db volumes: - mysql_data:/var/lib/mysql redis: image: redis:6.2 ports: - 6379:6379 volumes: - redis_data:/data volumes: mysql_data: redis_data:6.2 前端部署方案使用Nginx部署前端应用nginx.conf配置示例server { listen 80; server_name yourdomain.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api { proxy_pass http://backend: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 ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; add_header Cache-Control public, immutable; } }6.3 性能监控与日志收集我们使用Prometheus Grafana监控系统性能SpringBoot应用集成MicrometerConfiguration public class MetricsConfig { Bean MeterRegistryCustomizerMeterRegistry metricsCommonTags() { return registry - registry.config().commonTags( application, tourism-system, region, System.getenv(REGION) ! null ? System.getenv(REGION) : dev); } }配置Prometheus抓取指标scrape_configs: - job_name: tourism-system metrics_path: /actuator/prometheus static_configs: - targets: [app:8080]使用ELK收集日志// logback-spring.xml配置 configuration appender nameJSON classch.qos.logback.core.ConsoleAppender encoder classnet.logstash.logback.encoder.LogstashEncoder/ /appender root levelINFO appender-ref refJSON/ /root /configuration7. 开发经验与问题解决7.1 跨域问题解决方案在前后端分离架构中跨域是常见问题。我们的解决方案后端配置CORSConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .exposedHeaders(Authorization) .maxAge(3600); } }前端代理配置开发环境// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })7.2 图片上传与存储方案我们采用阿里云OSS存储用户上传的图片后端实现文件上传接口RestController RequestMapping(/api/upload) public class UploadController { Autowired private OssService ossService; PostMapping public ResponseEntityUploadResult uploadFile(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { throw new BusinessException(上传文件不能为空); } String originalFilename file.getOriginalFilename(); String fileExtension originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID() fileExtension; try { String url ossService.upload(file.getInputStream(), fileName); UploadResult result new UploadResult(fileName, url); return ResponseEntity.ok(result); } catch (IOException e) { throw new BusinessException(文件上传失败); } } }前端上传组件实现script setup import { ref } from vue import { ElMessage } from element-plus const emit defineEmits([success]) const fileList ref([]) const loading ref(false) const handleUpload async (file) { try { loading.value true const formData new FormData() formData.append(file, file.raw) const response await uploadFile(formData) emit(success, response.data) fileList.value [] } catch (error) { ElMessage.error(error.message || 上传失败) } finally { loading.value false } } /script template el-upload v-model:file-listfileList action# :auto-uploadfalse :limit1 :on-changehandleUpload :show-file-listfalse el-button typeprimary :loadingloading el-iconUpload //el-icon 上传图片 /el-button /el-upload /template7.3 高并发场景优化针对节假日等高并发场景我们做了以下优化使用Redis缓存热门景点数据Service public class ScenicSpotServiceImpl implements ScenicSpotService { Autowired private ScenicSpotRepository scenicSpotRepository; Autowired private RedisTemplateString, Object redisTemplate; private static final String CACHE_KEY scenic:top:10; private static final long CACHE_EXPIRE 3600; // 1小时 Override Cacheable(value scenic, key #id) public ScenicSpot getScenicSpotById(Long id) { return scenicSpotRepository.findById(id) .orElseThrow(() - new ResourceNotFoundException(景点不存在)); } Override public ListScenicSpot getTopScenicSpots() { // 先查缓存 ListScenicSpot cachedList (ListScenicSpot) redisTemplate.opsForValue().get(CACHE_KEY); if (cachedList ! null) { return cachedList; } // 查数据库 ListScenicSpot list scenicSpotRepository.findTop10ByOrderByRatingDesc(); // 写入缓存 redisTemplate.opsForValue().set(CACHE_KEY, list, CACHE_EXPIRE, TimeUnit.SECONDS); return list; } }使用分布式锁防止超卖Service public class OrderServiceImpl implements OrderService { Autowired private RedissonClient redissonClient; Override public Order createScenicOrder(Long userId, ScenicOrderDTO orderDTO) { String lockKey scenic:lock: orderDTO.getScenicId(); RLock lock redissonClient.getLock(lockKey); try { // 尝试加锁最多等待5秒锁有效期30秒 boolean locked lock.tryLock(5, 30, TimeUnit.SECONDS); if (!locked) { throw new BusinessException(系统繁忙请稍后再试); } // 检查库存 ScenicSpot scenicSpot scenicSpotRepository.findById(orderDTO.getScenicId()) .orElseThrow(() - new ResourceNotFoundException(景点不存在)); if (scenicSpot.getDailyLimit() 0) { throw new BusinessException(今日门票已售罄); } // 扣减库存 scenicSpot.setDailyLimit(scenicSpot.getDailyLimit() - orderDTO.getQuantity()); scenicSpotRepository.save(scenicSpot); // 创建订单 // ... return order; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new BusinessException(下单失败); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } }使用消息队列削峰填谷Service public class OrderServiceImpl implements OrderService { Autowired private RabbitTemplate rabbitTemplate; Override public void asyncCreateOrder(OrderRequest request) { rabbitTemplate.convertAndSend( order.exchange, order.create, request, message - { message.getMessageProperties().setDelay(5000); // 延迟5秒 return message; }); } RabbitListener(queues order.create.queue) public void processOrderCreation(OrderRequest request) { try { createOrder(request.getUserId(), request.getOrderDTO()); } catch (Exception e) { // 记录错误日志 log.error(订单处理失败: {}, e.getMessage()); // 加入重试队列 rabbitTemplate.convertAndSend( order.exchange, order.retry, request); } } }8. 项目总结与展望这个旅游信息管理系统从零开始构建前后历时6个月期间遇到了各种技术挑战和业务问题。通过这个项目我深刻体会到几个关键点技术选型要结合实际业务需求不要盲目追求新技术。我们最初考虑过使用GraphQL但评估后发现RESTful API完全能满足需求且团队熟悉度更高。数据库设计要预留扩展空间。旅游业务变化快我们在设计时预留了多个扩展字段后期新增需求时基本不需要修改表结构。性能优化要循序渐进。不要一开始就过度优化应该先实现功能再根据实际性能测试结果有针对性地优化。监控系统必不可少。上线初期我们忽视了监控导致一些问题不能及时发现。后来引入PrometheusGrafana后系统稳定性大幅提升。未来可以考虑的改进方向引入推荐算法根据用户历史行为推荐个性化旅游路线增加社交功能允许用户分享旅行经历和攻略接入更多第三方服务如天气、交通实时信息等开发移动端APP提升用户体验这个项目让我对旅游行业的信息化建设有了更深的理解也积累了丰富的全栈开发经验。希望这些经验分享对正在开发类似系统的同行有所帮助。
网站建设高端定制企业官网