1. 项目背景与核心价值
作为一个在旅游行业摸爬滚打多年的从业者,我深知地方旅游推广和特产销售的痛点。传统旅游网站往往只提供简单的景点介绍,而电商平台又缺乏地域文化特色,两者割裂严重。这个项目正是为了解决这个痛点而生——通过前后端分离架构,打造一个集旅游信息展示与特产购物于一体的综合性平台。
选择SpringBoot+Vue这套技术栈不是偶然。后端需要稳定高效的业务处理能力,SpringBoot的自动配置和嵌入式Tomcat让部署变得极其简单;前端需要灵活的用户交互体验,Vue的组件化开发模式正好满足这个需求。前后端分离的架构设计,让团队可以并行开发,大大提升了项目效率。
2. 技术架构设计解析
2.1 后端技术选型与实现
SpringBoot作为后端框架,我选择了2.7.0版本——稳定且功能完善。数据库方面,考虑到旅游数据的关联性,MySQL 8.0是最佳选择。这里有个小技巧:使用JSON字段存储景点的开放时间等非结构化数据,既灵活又便于查询。
java复制// 景点实体类示例
@Entity
public class ScenicSpot {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String location;
@Column(columnDefinition = "json")
private String openingHours; // 存储为JSON格式
// getters & setters
}
认证授权采用Spring Security + JWT的方案。特别注意:特产购买接口需要额外的支付权限校验,这里我设计了一个自定义注解:
java复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('USER') && @paymentService.checkBalance(authentication.principal.id)")
public @interface CanPurchase {
}
2.2 前端架构设计要点
Vue 3的组合式API让代码组织更加清晰。项目采用以下目录结构:
code复制src/
├── api/ # 接口请求封装
├── assets/ # 静态资源
├── components/ # 公共组件
├── composables/ # 组合式函数
├── router/ # 路由配置
├── stores/ # Pinia状态管理
└── views/ # 页面组件
地图展示使用高德地图API,这里有个性能优化技巧:懒加载地图组件,只有当用户滚动到地图区域时才初始化。
javascript复制// 懒加载地图组件
const AMapLoader = () => import('@amap/amap-jsapi-loader')
onMounted(async () => {
if (isInViewport('#map-container')) {
const AMap = await AMapLoader()
// 初始化地图...
}
})
3. 核心功能实现细节
3.1 旅游信息展示模块
景点数据采用多级缓存策略:Redis缓存热门景点 -> MySQL持久化存储 -> 本地JSON兜底数据。接口设计遵循RESTful规范,但针对复杂查询做了特殊处理:
java复制@GetMapping("/scenic-spots")
public Page<ScenicSpot> getSpots(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String category,
@PageableDefault(size = 10) Pageable pageable) {
if (keyword != null) {
return spotService.search(keyword, pageable);
}
return spotService.findByCategory(category, pageable);
}
前端实现无限滚动加载时,注意添加防抖处理:
javascript复制const handleScroll = _.debounce(() => {
if (reachBottom() && !loading.value) {
loadMore()
}
}, 300)
3.2 特产电商模块
购物车设计采用本地存储+服务端同步的策略,提升用户体验:
javascript复制// 购物车状态管理
const useCartStore = defineStore('cart', {
state: () => ({
items: JSON.parse(localStorage.getItem('cart')) || []
}),
actions: {
async syncWithServer() {
if (isAuthenticated) {
await api.syncCart(this.items)
}
}
}
})
支付流程特别注意安全验证:
- 前端收集支付信息后立即清除敏感数据
- 后端验证价格与商品是否匹配
- 使用第三方支付平台SDK时校验签名
4. 前后端协同开发实践
4.1 接口规范与Mock数据
我们使用Swagger进行API文档管理,同时配置了easy-mock模拟接口数据。开发阶段的前端配置:
javascript复制// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: process.env.MOCK_MODE
? 'http://localhost:3000'
: 'http://real-api.com',
changeOrigin: true
}
}
}
})
4.2 跨域解决方案
生产环境使用Nginx反向代理解决跨域:
nginx复制location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
开发环境配置Vite代理时,注意处理WebSocket连接:
javascript复制proxy: {
'/ws': {
target: 'ws://localhost:8080',
ws: true
}
}
5. 性能优化实战经验
5.1 图片加载优化
旅游平台图片资源多,我们采用以下策略:
- CDN加速静态资源
- 使用WebP格式图片
- 实现懒加载和渐进式加载
html复制<img
v-lazy="imageUrl"
:src="placeholder"
loading="lazy"
class="progressive"
/>
5.2 数据库查询优化
针对景点列表查询,建立了复合索引:
sql复制CREATE INDEX idx_spot_search ON scenic_spot(name, rating, province);
使用Spring Data JPA的@EntityGraph解决N+1查询问题:
java复制@EntityGraph(attributePaths = {"images", "reviews"})
@Query("SELECT s FROM ScenicSpot s WHERE s.province = :province")
List<ScenicSpot> findByProvinceWithDetails(String province);
6. 部署与运维方案
6.1 容器化部署
Docker Compose编排文件示例:
yaml复制version: '3'
services:
backend:
build: ./backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
6.2 监控与日志
SpringBoot应用集成Prometheus监控:
java复制@Bean
MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags(
"application", "tourism-platform"
);
}
前端使用Sentry捕获异常:
javascript复制import * as Sentry from "@sentry/vue";
Sentry.init({
app,
dsn: "your-dsn",
integrations: [
new Sentry.BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(router)
})
],
tracesSampleRate: 0.2
});
7. 典型问题排查实录
7.1 支付回调丢失问题
现象:用户支付成功后,订单状态未更新
排查过程:
- 检查支付平台回调日志
- 验证签名算法
- 发现Nginx配置限制回调超时时间
解决方案:
nginx复制location /api/payment/callback {
proxy_pass http://backend:8080;
proxy_read_timeout 60s;
proxy_connect_timeout 60s;
}
7.2 地图加载性能问题
现象:移动端地图加载缓慢
优化措施:
- 按需加载地图插件
- 使用矢量地图替代栅格地图
- 实现地图瓦片缓存
javascript复制const map = new AMap.Map('container', {
viewMode: '2D', // 默认使用2D模式
zoom: 11, // 初始化缩放级别
features: ['bg', 'road', 'building'], // 按需加载要素
cacheEnabled: true // 开启缓存
});
8. 项目扩展方向
基于现有平台,可以考虑加入以下功能:
- AR实景导航:通过手机摄像头叠加景点信息
- 旅游路线规划:基于用户偏好生成个性化路线
- 直播带货:让特产商家实时展示商品
技术储备建议:
- WebRTC实现实时视频
- 图数据库存储路线关系
- 推荐算法优化商品展示
在实现特产推荐功能时,我们尝试了两种方案:
java复制// 基于内容的推荐
public List<Product> recommendByContent(Long productId) {
Product current = productRepo.findById(productId);
return productRepo.findByTagsIn(current.getTags());
}
// 协同过滤推荐
public List<Product> recommendByCF(Long userId) {
User user = userRepo.findByIdWithBehavior(userId);
return recommendationEngine.recommend(user);
}
实际测试发现,对于新上架商品,基于内容的推荐效果更好;而对于老用户,协同过滤的准确率更高。这个经验告诉我们:推荐系统需要根据业务场景灵活选择算法。