三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

基于SpringBoot+Vue的露营探险系统的设计与实现

基于SpringBoot+Vue的露营探险系统的设计与实现

一、项目背景与意义

随着户外运动的兴起和人们对自然生活方式的向往,露营探险活动日益普及。然而,传统的露营活动组织、营地信息查询、装备管理、行程规划等环节往往依赖零散的社交群组、纸质记录或简单的电子表格,存在信息不对称、管理效率低下、安全难以保障等问题。

因此,开发一个集营地信息管理、活动组织、装备租赁、行程规划、社交分享于一体的综合性露营探险系统,具有重要的现实意义:

  • 提升用户体验:为用户提供一站式的露营探险服务,简化从计划到出行的全流程。
  • 促进信息共享:构建一个公开、透明的营地与活动信息平台,打破信息壁垒。
  • 保障活动安全:通过行程规划、天气预报集成、紧急联系人等功能,提升户外活动的安全性。
  • 推动产业发展:为营地经营者、装备商、活动组织者提供一个高效的商业对接平台。

本系统旨在利用现代Web技术,构建一个功能完善、用户体验优良、可扩展性强的露营探险管理平台。

二、技术栈选型

系统采用前后端分离的架构设计,后端提供RESTful API,前端负责用户交互与数据展示。

2.1 后端技术栈 (SpringBoot)

  • 核心框架:Spring Boot 2.7.x / 3.x
  • Web框架:Spring MVC
  • 数据持久层:Spring Data JPA + Hibernate
  • 数据库:MySQL 8.0
  • 缓存:Redis (用于会话管理、热点数据缓存)
  • 安全框架:Spring Security + JWT (JSON Web Token)
  • API文档:SpringDoc OpenAPI (Swagger UI)
  • 构建工具:Maven 或 Gradle
  • 其他:Lombok (简化代码), MapStruct (对象映射), Hutool (工具集)

2.2 前端技术栈 (Vue.js)

  • 核心框架:Vue 3 (Composition API)
  • 构建工具:Vite
  • 路由:Vue Router 4
  • 状态管理:Pinia
  • UI组件库:Element Plus 或 Ant Design Vue
  • HTTP客户端:Axios
  • 地图集成:高德地图 JavaScript API 或 Leaflet
  • 图表:ECharts 或 AntV
  • 包管理:npm 或 yarn

2.3 开发与部署

  • 版本控制:Git
  • 接口调试:Postman / Apifox
  • 容器化:Docker, Docker Compose
  • 部署:后端可部署于Linux服务器(如使用Nginx反向代理),前端静态资源可部署于Nginx或对象存储(如OSS、COS)。

三、系统核心功能模块设计

系统主要分为前台用户端和后台管理端。

3.1 前台用户端功能

  • 用户中心:注册、登录(含第三方登录)、个人信息管理。
  • 营地探索:按位置、评分、设施等条件筛选和查看营地详情(图片、介绍、价格、评论)。
  • 活动广场:浏览、搜索、报名参加由用户或官方发布的露营活动。
  • 装备商城/租赁:浏览、购买或租赁露营装备,管理购物车与订单。
  • 行程规划:在地图上规划露营路线,添加途经点,估算时间和费用。
  • 社区分享:发布露营游记、图片、视频,点赞、评论、收藏。
  • 消息通知:系统通知、活动提醒、订单状态变更等。

3.2 后台管理端功能

  • 仪表盘:关键数据概览(用户数、订单量、营地数等)。
  • 营地管理:营地的增删改查、审核、上下架。
  • 活动管理:活动的审核、推荐、置顶。
  • 商品管理:装备商品的分类、SKU、库存、价格管理。
  • 订单管理:处理用户订单(租赁、购买)、退款。
  • 用户管理:用户列表、权限控制、封禁/解封。
  • 内容审核:审核用户发布的游记、评论等内容。
  • 系统设置:轮播图、公告、基础参数配置。

四、数据库核心表设计

以下是部分核心数据表的结构概览:

-- 用户表 CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(255) NOT NULL COMMENT '加密后的密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(500) DEFAULT NULL COMMENT '头像URL', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `email` varchar(100) DEFAULT NULL 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 `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 营地信息表 CREATE TABLE `camp_site` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '营地名称', `province` varchar(20) DEFAULT NULL COMMENT '省', `city` varchar(20) DEFAULT NULL COMMENT '市', `district` varchar(20) DEFAULT NULL COMMENT '区', `address` varchar(255) DEFAULT NULL COMMENT '详细地址', `longitude` decimal(10,7) DEFAULT NULL COMMENT '经度', `latitude` decimal(10,7) DEFAULT NULL COMMENT '纬度', `description` text COMMENT '营地描述', `facilities` varchar(500) DEFAULT NULL COMMENT '设施(JSON数组,如["WiFi","淋浴","烧烤"])', `price_per_night` decimal(10,2) DEFAULT NULL COMMENT '每晚价格', `cover_image` varchar(500) DEFAULT NULL COMMENT '封面图URL', `status` tinyint DEFAULT '0' COMMENT '状态 (0:待审核,1:已上架,2:已下架)', `create_user_id` bigint DEFAULT NULL COMMENT '创建人', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营地信息表'; -- 活动表 CREATE TABLE `camp_activity` ( `id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL COMMENT '活动标题', `description` text COMMENT '活动详情', `cover_image` varchar(500) DEFAULT NULL COMMENT '封面图', `start_time` datetime NOT NULL COMMENT '开始时间', `end_time` datetime NOT NULL COMMENT '结束时间', `location` varchar(255) DEFAULT NULL COMMENT '集合地点', `max_participants` int DEFAULT NULL COMMENT '最大参与人数', `current_participants` int DEFAULT '0' COMMENT '当前报名人数', `fee` decimal(10,2) DEFAULT '0.00' COMMENT '费用', `status` tinyint DEFAULT '0' COMMENT '状态 (0:草稿,1:已发布,2:已结束,3:已取消)', `organizer_id` bigint NOT NULL COMMENT '组织者ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_organizer` (`organizer_id`), KEY `idx_time` (`start_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='露营活动表';

五、核心代码示例

5.1 后端SpringBoot核心代码

1. JPA实体类 (CampSite.java)

import lombok.Data; import javax.persistence.*; import java.math.BigDecimal; import java.time.LocalDateTime; @Entity @Table(name = "camp_site") @Data public class CampSite { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 100) private String name; private String province; private String city; private String district; private String address; @Column(precision = 10, scale = 7) private BigDecimal longitude; @Column(precision = 10, scale = 7) private BigDecimal latitude; @Lob private String description; @Column(length = 500) private String facilities; // 存储为JSON字符串,如 ["WiFi","淋浴"] @Column(name = "price_per_night", precision = 10, scale = 2) private BigDecimal pricePerNight; @Column(name = "cover_image", length = 500) private String coverImage; private Integer status = 0; // 0:待审核,1:已上架,2:已下架 @Column(name = "create_user_id") private Long createUserId; @Column(name = "create_time", updatable = false) private LocalDateTime createTime = LocalDateTime.now(); @Column(name = "update_time") private LocalDateTime updateTime = LocalDateTime.now(); }

2. Spring Data JPA 仓库接口 (CampSiteRepository.java)

import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.math.BigDecimal; import java.util.List; public interface CampSiteRepository extends JpaRepository<CampSite, Long>, JpaSpecificationExecutor<CampSite> { Page<CampSite> findByStatusOrderByCreateTimeDesc(Integer status, Pageable pageable); // 根据城市和状态查询 Page<CampSite> findByCityAndStatus(String city, Integer status, Pageable pageable); // 复杂查询:根据地理范围查找营地 (简化示例) @Query("SELECT cs FROM CampSite cs WHERE cs.status = 1 " + "AND cs.latitude BETWEEN :minLat AND :maxLat " + "AND cs.longitude BETWEEN :minLng AND :maxLng") List<CampSite> findWithinBounds(@Param("minLat") BigDecimal minLat, @Param("maxLat") BigDecimal maxLat, @Param("minLng") BigDecimal minLng, @Param("maxLng") BigDecimal maxLng); }

3. Spring Security JWT 配置与过滤器 (JwtAuthenticationFilter.java)

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenUtil jwtTokenUtil; private final UserDetailsService userDetailsService; public JwtAuthenticationFilter(JwtTokenUtil jwtTokenUtil, UserDetailsService userDetailsService) { this.jwtTokenUtil = jwtTokenUtil; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); String username = jwtTokenUtil.getUsernameFromToken(token); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(token, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } } filterChain.doFilter(request, response); } }

5.2 前端Vue核心代码

1. 营地列表页面组件 (CampSiteList.vue)

<template> <div class="camp-site-list"> <div class="filter-bar"> <el-input v-model="searchParams.keyword" placeholder="搜索营地名称" style="width: 200px;" @keyup.enter="handleSearch" /> <el-select v-model="searchParams.city" placeholder="选择城市" clearable @change="handleSearch"> <el-option v-for="city in cityOptions" :key="city.value" :label="city.label" :value="city.value" /> </el-select> <el-button type="primary" @click="handleSearch">搜索</el-button> </div> <el-row :gutter="20"> <el-col :span="8" v-for="site in campSiteList" :key="site.id"> <el-card class="camp-card" :body-style="{ padding: '0px' }" @click="goToDetail(site.id)"> <img :src="site.coverImage || defaultImage" class="camp-image" /> <div style="padding: 14px;"> <div class="camp-title">{{ site.name }}</div> <div class="camp-location"><i class="el-icon-location"></i> {{ site.city }} {{ site.district }}</div> <div class="camp-facilities"> <el-tag v-for="facility in (site.facilities || [])" :key="facility" size="small">{{ facility }}</el-tag> </div> <div class="camp-price"> <span class="price">¥{{ site.pricePerNight }}</span> / 晚 </div> </div> </el-card> </el-col> </el-row> <div class="pagination"> <el-pagination @current-change="handlePageChange" :current-page="pagination.current" :page-size="pagination.size" :total="pagination.total" layout="prev, pager, next" ></el-pagination> </div> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { getCampSiteList } from '@/api/camp' const router = useRouter() const defaultImage = 'https://via.placeholder.com/300x200?text=Camp+Site' const searchParams = ref({ keyword: '', city: '' }) const cityOptions = ref([ { label: '北京市', value: '北京' }, { label: '上海市', value: '上海' }, { label: '杭州市', value: '杭州' } ]) const campSiteList = ref([]) const pagination = ref({ current: 1, size: 9, total: 0 }) const loadCampSites = async () => { try { const params = {

← 返回列表