1. 项目背景与核心需求
桂林作为世界知名的旅游目的地,每年吸引着大量游客前来观光。但在实际旅游过程中,游客常常面临信息获取不便、路线规划困难等问题。传统纸质地图和分散的在线资源难以满足现代游客对即时性、个性化服务的需求。
这个桂林旅游景点导游平台正是为了解决这些痛点而设计。它采用前后端分离架构,后端使用SpringBoot提供RESTful API服务,前端基于Vue.js构建响应式界面,为游客提供一站式的旅游信息服务。
提示:选择SpringBoot+Vue的技术栈,主要考虑到SpringBoot在Java生态中的成熟度,以及Vue.js在构建用户友好界面方面的优势。这种组合既能保证后端服务的稳定性,又能提供流畅的前端体验。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计
2.1 技术选型分析
后端技术栈:
- SpringBoot 2.7.x:提供快速开发的脚手架
- Spring Security:负责系统安全认证
- JWT:实现无状态的身份验证
- MyBatis-Plus:简化数据库操作
- MySQL 8.0:关系型数据库存储核心数据
- Redis:缓存热点数据,提升系统响应速度
前端技术栈:
- Vue 3.x:前端主框架
- Element Plus:UI组件库
- Axios:HTTP客户端
- Vue Router:前端路由管理
- ECharts:数据可视化展示
2.2 系统模块划分
系统主要分为以下几个功能模块:
- 用户管理模块
- 景点信息模块
- 路线规划模块
- 在线购票模块
- 评论互动模块
- 后台管理模块
3. 数据库设计与实现
3.1 核心表结构
景点信息表(attraction)
sql复制CREATE TABLE `attraction` (
`attraction_id` int NOT NULL AUTO_INCREMENT,
`attraction_name` varchar(100) NOT NULL,
`location` varchar(200) NOT NULL,
`open_time` varchar(50) DEFAULT NULL,
`ticket_price` decimal(10,2) DEFAULT NULL,
`description` text,
`cover_image` varchar(255) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`attraction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
用户表(user)
sql复制CREATE TABLE `user` (
`user_id` int NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password_hash` varchar(255) NOT NULL,
`email` varchar(100) DEFAULT NULL,
`phone` varchar(20) DEFAULT NULL,
`role` varchar(20) DEFAULT 'user',
`register_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`user_id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
订单表(order)
sql复制CREATE TABLE `order` (
`order_id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`attraction_id` int NOT NULL,
`quantity` int NOT NULL,
`total_amount` decimal(10,2) NOT NULL,
`payment_status` varchar(20) DEFAULT 'unpaid',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`order_id`),
KEY `user_id` (`user_id`),
KEY `attraction_id` (`attraction_id`),
CONSTRAINT `order_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`),
CONSTRAINT `order_ibfk_2` FOREIGN KEY (`attraction_id`) REFERENCES `attraction` (`attraction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 数据库优化策略
- 索引优化:在经常查询的字段上建立合适索引
- 分表策略:对于可能大量增长的数据考虑分表
- 缓存机制:使用Redis缓存热点数据
- 读写分离:在高并发场景下考虑读写分离
4. 后端实现细节
4.1 SpringBoot项目结构
code复制src/main/java
├── com.guilin.tourism
│ ├── config # 配置类
│ ├── controller # 控制器
│ ├── service # 服务层
│ ├── dao # 数据访问层
│ ├── entity # 实体类
│ ├── dto # 数据传输对象
│ ├── vo # 视图对象
│ ├── util # 工具类
│ └── exception # 异常处理
4.2 核心功能实现
4.2.1 JWT认证实现
java复制@Component
public class JwtTokenProvider {
@Value("${app.jwt.secret}")
private String jwtSecret;
@Value("${app.jwt.expiration-in-ms}")
private int jwtExpirationInMs;
public String generateToken(UserPrincipal userPrincipal) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public Long getUserIdFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
logger.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
logger.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
logger.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
logger.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
logger.error("JWT claims string is empty.");
}
return false;
}
}
4.2.2 景点推荐算法
java复制@Service
public class AttractionRecommendationService {
@Autowired
private AttractionRepository attractionRepository;
@Autowired
private UserBehaviorRepository userBehaviorRepository;
public List<Attraction> recommendAttractions(Long userId, int limit) {
// 1. 获取用户历史行为数据
List<UserBehavior> behaviors = userBehaviorRepository.findByUserId(userId);
// 2. 基于内容的推荐
List<Attraction> contentBased = contentBasedRecommendation(behaviors, limit/2);
// 3. 基于热门的推荐
List<Attraction> popular = popularRecommendation(limit/2);
// 4. 合并结果并去重
List<Attraction> result = new ArrayList<>();
result.addAll(contentBased);
result.addAll(popular);
return result.stream()
.distinct()
.limit(limit)
.collect(Collectors.toList());
}
private List<Attraction> contentBasedRecommendation(List<UserBehavior> behaviors, int limit) {
if (behaviors.isEmpty()) {
return Collections.emptyList();
}
// 获取用户浏览最多的景点类别
Map<String, Long> categoryCount = behaviors.stream()
.filter(b -> b.getBehaviorType().equals("VIEW"))
.collect(Collectors.groupingBy(
b -> b.getAttraction().getCategory(),
Collectors.counting()
));
String favoriteCategory = categoryCount.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse("");
// 推荐同类别景点
return attractionRepository.findByCategory(favoriteCategory, PageRequest.of(0, limit));
}
private List<Attraction> popularRecommendation(int limit) {
return attractionRepository.findPopularAttractions(PageRequest.of(0, limit));
}
}
5. 前端实现细节
5.1 Vue项目结构
code复制src/
├── api/ # API请求
├── assets/ # 静态资源
├── components/ # 公共组件
├── router/ # 路由配置
├── store/ # Vuex状态管理
├── utils/ # 工具函数
├── views/ # 页面组件
├── App.vue # 根组件
└── main.js # 入口文件
5.2 核心页面实现
5.2.1 景点列表页
vue复制<template>
<div class="attraction-list">
<el-row :gutter="20">
<el-col :span="6" v-for="item in attractions" :key="item.attraction_id">
<el-card :body-style="{ padding: '0px' }" shadow="hover">
<img :src="item.cover_image" class="image" @click="goDetail(item.attraction_id)">
<div style="padding: 14px;">
<span>{{ item.attraction_name }}</span>
<div class="bottom">
<span class="price">¥{{ item.ticket_price }}</span>
<el-button type="text" class="button" @click="goDetail(item.attraction_id)">查看详情</el-button>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-pagination
@current-change="handlePageChange"
:current-page="pagination.current"
:page-size="pagination.size"
layout="total, prev, pager, next"
:total="pagination.total">
</el-pagination>
</div>
</template>
<script>
import { getAttractionList } from '@/api/attraction'
export default {
data() {
return {
attractions: [],
pagination: {
current: 1,
size: 12,
total: 0
}
}
},
created() {
this.fetchData()
},
methods: {
fetchData() {
getAttractionList({
page: this.pagination.current,
size: this.pagination.size
}).then(response => {
this.attractions = response.data.records
this.pagination.total = response.data.total
})
},
handlePageChange(current) {
this.pagination.current = current
this.fetchData()
},
goDetail(id) {
this.$router.push(`/attraction/detail/${id}`)
}
}
}
</script>
5.2.2 地图集成实现
vue复制<template>
<div class="map-container">
<div id="map" style="width: 100%; height: 500px;"></div>
<el-card class="map-control">
<el-checkbox-group v-model="selectedCategories">
<el-checkbox v-for="category in categories"
:key="category"
:label="category">
{{ category }}
</el-checkbox>
</el-checkbox-group>
</el-card>
</div>
</template>
<script>
export default {
data() {
return {
map: null,
markers: [],
categories: ['自然风光', '历史文化', '休闲娱乐', '美食购物'],
selectedCategories: ['自然风光', '历史文化']
}
},
mounted() {
this.initMap()
this.loadAttractions()
},
methods: {
initMap() {
// 使用高德地图API初始化地图
this.map = new AMap.Map('map', {
zoom: 12,
center: [110.299, 25.274]
})
// 添加缩放控件
this.map.addControl(new AMap.ControlBar({
showZoomBar: true,
showControlButton: true
}))
},
loadAttractions() {
getAttractionList({
categories: this.selectedCategories
}).then(response => {
this.clearMarkers()
this.addMarkers(response.data)
})
},
addMarkers(attractions) {
attractions.forEach(attraction => {
const marker = new AMap.Marker({
position: new AMap.LngLat(attraction.longitude, attraction.latitude),
title: attraction.attraction_name,
content: `<div class="marker-content">${attraction.attraction_name}</div>`
})
marker.on('click', () => {
this.$router.push(`/attraction/detail/${attraction.attraction_id}`)
})
this.map.add(marker)
this.markers.push(marker)
})
},
clearMarkers() {
this.markers.forEach(marker => {
this.map.remove(marker)
})
this.markers = []
}
},
watch: {
selectedCategories() {
this.loadAttractions()
}
}
}
</script>
6. 系统部署方案
6.1 开发环境部署
-
后端环境:
- JDK 1.8+
- Maven 3.6+
- MySQL 8.0
- Redis 6.0+
-
前端环境:
- Node.js 14.x+
- npm 6.x+
6.2 生产环境部署
推荐使用Docker容器化部署:
dockerfile复制# 后端Dockerfile
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
dockerfile复制# 前端Dockerfile
FROM nginx:alpine
COPY dist/ /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
6.3 Nginx配置示例
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
root /usr/share/nginx/html;
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 /static/ {
alias /usr/share/nginx/html/static/;
expires 30d;
}
}
7. 项目开发经验分享
7.1 开发中遇到的典型问题
-
跨域问题解决方案:
- 开发阶段:配置SpringBoot的CORS过滤器
- 生产环境:通过Nginx反向代理解决
-
前端路由与后端路由冲突:
- 配置Vue Router使用history模式
- Nginx配置try_files回退到index.html
-
地图API加载性能优化:
- 使用异步加载方式
- 实现按需加载地图组件
7.2 性能优化实践
-
数据库层面:
- 合理设计索引
- 使用连接池配置
- 实现读写分离
-
应用层面:
- 使用Redis缓存热点数据
- 实现分页查询
- 启用Gzip压缩
-
前端层面:
- 组件懒加载
- 路由懒加载
- 图片懒加载
7.3 安全防护措施
-
接口安全:
- JWT认证
- 接口权限控制
- 参数校验
-
数据安全:
- 密码加密存储
- SQL注入防护
- XSS防护
-
传输安全:
- HTTPS强制启用
- 敏感数据加密传输
8. 项目扩展方向
-
移动端适配:
- 开发微信小程序版本
- 开发React Native跨平台应用
-
智能推荐增强:
- 引入机器学习算法
- 增加用户画像系统
-
社交功能扩展:
- 用户互动社区
- 游记分享功能
-
商业化功能:
- 景区周边商品电商
- 广告投放系统
提示:在实际开发过程中,建议采用迭代式开发方法,先实现核心功能,再逐步扩展。同时要注意保持代码的可维护性和可扩展性,为后续功能开发预留接口。
