1. 项目背景与技术选型
洛阳作为十三朝古都,拥有丰富的历史文化资源,开发一个现代化的旅游网站具有重要的现实意义。这个基于Spring Boot和Vue.js的洛阳旅游网站项目,采用了前后端分离的架构模式,能够充分发挥两种技术的优势。
选择Spring Boot作为后端框架主要基于以下考虑:
- 快速开发:Spring Boot的自动配置和起步依赖大大简化了项目搭建过程
- 微服务友好:便于后期扩展为微服务架构
- 生态丰富:Spring生态提供了完善的安全、数据库、缓存等解决方案
- 性能稳定:经过大量企业级应用验证
Vue.js作为前端框架的优势在于:
- 渐进式框架:可以逐步采用,学习曲线平缓
- 组件化开发:便于复用和维护
- 响应式设计:数据驱动视图,开发效率高
- 活跃社区:丰富的第三方组件和插件
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统架构设计
2.1 整体架构
系统采用典型的三层架构:
- 表现层:Vue.js实现用户界面
- 业务逻辑层:Spring Boot处理核心业务
- 数据访问层:MyBatis/JPA操作数据库
前后端通过RESTful API进行通信,使用JSON格式交换数据。这种分离架构使得前后端可以独立开发和部署,提高了开发效率。
2.2 技术栈组合
后端技术栈:
- 核心框架:Spring Boot 2.7.x
- ORM框架:MyBatis-Plus
- 数据库:MySQL 8.0
- 缓存:Redis
- 安全框架:Spring Security
- 构建工具:Maven
前端技术栈:
- 核心框架:Vue.js 2.6.x
- UI组件库:Element UI
- 状态管理:Vuex
- 路由:Vue Router
- 构建工具:Webpack
3. 核心功能模块实现
3.1 旅游景点展示模块
景点模块是网站的核心功能,主要包含:
- 景点列表:分页展示、筛选排序
- 景点详情:图文介绍、360°全景展示
- 景点搜索:支持关键字、分类、区域多维度搜索
后端实现要点:
java复制@RestController
@RequestMapping("/api/attractions")
public class AttractionController {
@Autowired
private AttractionService attractionService;
@GetMapping
public PageResult<AttractionVO> listAttractions(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Integer categoryId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
return attractionService.listAttractions(keyword, categoryId, page, size);
}
@GetMapping("/{id}")
public AttractionDetailVO getAttractionDetail(@PathVariable Long id) {
return attractionService.getAttractionDetail(id);
}
}
前端实现要点:
javascript复制<template>
<div class="attraction-list">
<el-row :gutter="20">
<el-col
v-for="item in attractions"
:key="item.id"
:span="8">
<attraction-card :data="item" />
</el-col>
</el-row>
<el-pagination
@current-change="handlePageChange"
:current-page="pagination.current"
:page-size="pagination.size"
:total="pagination.total">
</el-pagination>
</div>
</template>
<script>
export default {
data() {
return {
attractions: [],
pagination: {
current: 1,
size: 10,
total: 0
}
}
},
methods: {
async fetchAttractions() {
const { data } = await this.$http.get('/api/attractions', {
params: {
page: this.pagination.current,
size: this.pagination.size
}
})
this.attractions = data.list
this.pagination.total = data.total
},
handlePageChange(page) {
this.pagination.current = page
this.fetchAttractions()
}
},
created() {
this.fetchAttractions()
}
}
</script>
3.2 旅游路线规划模块
路线规划模块帮助游客合理安排行程:
- 智能推荐:基于用户偏好和历史数据推荐路线
- 自定义路线:用户可自行添加景点创建个性化路线
- 路线分享:支持将路线分享到社交平台
后端关键实现:
java复制@Service
public class RouteServiceImpl implements RouteService {
@Override
public RouteVO recommendRoute(Long userId) {
// 获取用户偏好
UserPreference preference = preferenceService.getByUserId(userId);
// 基于协同过滤算法推荐景点
List<Attraction> attractions = cfRecommender.recommend(preference);
// 基于景点位置进行路径规划
return routePlanner.plan(attractions);
}
}
前端路线展示组件:
javascript复制<template>
<div class="route-map">
<baidu-map
:center="center"
:zoom="zoom"
@ready="initMap">
<bm-navigation anchor="BMAP_ANCHOR_TOP_RIGHT"></bm-navigation>
<bm-driving
v-if="showRoute"
:start="startPoint"
:end="endPoint"
:waypoints="wayPoints"
:auto-viewport="true"
policy="BMAP_DRIVING_POLICY_LEAST_TIME">
</bm-driving>
</baidu-map>
</div>
</template>
4. 关键技术难点与解决方案
4.1 高并发景点详情页优化
旅游旺季时,热门景点页面访问量激增,我们采用多级缓存策略:
- 本地缓存:使用Caffeine缓存热点数据
java复制@Configuration
public class CacheConfig {
@Bean
public Cache<String, AttractionDetailVO> attractionCache() {
return Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
}
}
- Redis集群:缓存完整页面数据
java复制@Service
public class AttractionServiceImpl implements AttractionService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public AttractionDetailVO getAttractionDetail(Long id) {
String cacheKey = "attraction:" + id;
// 先查Redis
AttractionDetailVO detail = (AttractionDetailVO) redisTemplate.opsForValue().get(cacheKey);
if (detail != null) {
return detail;
}
// 查数据库
detail = attractionMapper.selectDetailById(id);
// 写入Redis
redisTemplate.opsForValue().set(cacheKey, detail, 1, TimeUnit.HOURS);
return detail;
}
}
- 静态化处理:对不常变的内容生成静态HTML
4.2 跨域问题解决
前后端分离架构下,跨域是必须解决的问题。我们采用以下方案:
- Spring Boot配置全局CORS
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true)
.maxAge(3600);
}
}
- 前端axios配置
javascript复制const http = axios.create({
baseURL: process.env.VUE_APP_API_BASE_URL,
timeout: 10000,
withCredentials: true
})
// 请求拦截器
http.interceptors.request.use(config => {
config.headers['X-Requested-With'] = 'XMLHttpRequest'
return config
}, error => {
return Promise.reject(error)
})
// 响应拦截器
http.interceptors.response.use(response => {
return response.data
}, error => {
return Promise.reject(error)
})
5. 项目部署与优化
5.1 后端部署方案
采用Docker容器化部署,提高可移植性和扩展性:
- Dockerfile配置
dockerfile复制FROM openjdk:11-jdk
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
- 使用Docker Compose编排服务
yaml复制version: '3'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- redis
- mysql
redis:
image: redis:6
ports:
- "6379:6379"
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: luoyang_tour
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
5.2 前端性能优化
- 代码分割
javascript复制// vue.config.js
module.exports = {
configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 30000,
maxSize: 244000,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
}
}
- 图片懒加载
javascript复制// 全局注册懒加载指令
Vue.directive('lazy', {
inserted: function (el, binding) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
el.src = binding.value
observer.unobserve(el)
}
})
})
observer.observe(el)
}
})
- Gzip压缩
javascript复制// 安装compression-webpack-plugin
const CompressionPlugin = require('compression-webpack-plugin')
module.exports = {
configureWebpack: {
plugins: [
new CompressionPlugin({
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8
})
]
}
}
6. 项目扩展方向
6.1 移动端适配
- 响应式布局优化
css复制/* 使用rem单位 */
html {
font-size: 16px;
}
@media screen and (max-width: 768px) {
html {
font-size: 14px;
}
}
/* 弹性布局 */
.attraction-card {
width: 100%;
@media (min-width: 576px) {
width: 50%;
}
@media (min-width: 992px) {
width: 33.33%;
}
}
- PWA支持
javascript复制// 注册Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('SW registered: ', registration);
}).catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}
6.2 智能推荐增强
- 用户行为分析
java复制@Aspect
@Component
public class UserBehaviorAspect {
@Autowired
private UserBehaviorService behaviorService;
@AfterReturning(
pointcut = "execution(* com.luoyang.tour.controller.*.*(..)) && " +
"@annotation(org.springframework.web.bind.annotation.GetMapping)",
returning = "result")
public void recordView(JoinPoint joinPoint, Object result) {
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
Long userId = getCurrentUserId();
String uri = request.getRequestURI();
behaviorService.recordView(userId, uri);
}
}
- 混合推荐算法
java复制public class HybridRecommender {
public List<Attraction> recommend(UserPreference preference) {
// 基于内容的推荐
List<Attraction> contentBased = contentBasedRecommender.recommend(preference);
// 协同过滤推荐
List<Attraction> cfBased = cfRecommender.recommend(preference);
// 混合结果
return mergeRecommendations(contentBased, cfBased);
}
private List<Attraction> mergeRecommendations(List<Attraction> list1, List<Attraction> list2) {
// 使用加权混合策略
Map<Long, Attraction> map = new HashMap<>();
list1.forEach(item -> {
map.putIfAbsent(item.getId(), item);
item.setScore(item.getScore() * 0.6);
});
list2.forEach(item -> {
if (map.containsKey(item.getId())) {
map.get(item.getId()).setScore(
map.get(item.getId()).getScore() + item.getScore() * 0.4);
} else {
item.setScore(item.getScore() * 0.4);
map.put(item.getId(), item);
}
});
return map.values().stream()
.sorted(Comparator.comparingDouble(Attraction::getScore).reversed())
.collect(Collectors.toList());
}
}
在实际开发过程中,我们遇到了不少挑战,比如初期对Vue和Spring Boot的整合不够熟悉,导致接口对接效率低下。后来通过建立明确的接口文档规范和使用Swagger UI自动生成API文档,大大提高了前后端协作效率。另一个经验是,对于旅游网站这种内容型项目,要特别注重SEO优化,我们通过服务端渲染(SSR)和合理的meta标签设置,显著提升了网站在搜索引擎中的排名。
