1. 项目概述:基于协同过滤的重庆旅游推荐系统
这个项目是我去年为重庆本地一家旅游服务平台开发的智能推荐系统,核心目标是通过分析用户历史行为数据,为游客个性化推荐重庆地区的旅游景点。系统采用前后端分离架构,前端使用Vue.js+Element UI构建响应式界面,后端基于Spring Boot实现推荐算法和API服务,数据层使用MySQL存储结构化数据,Redis作为缓存加速推荐结果获取。
在实际运营中,这套系统将用户对景点的点击、收藏、评分等行为转化为用户-物品交互矩阵,通过协同过滤算法挖掘用户潜在兴趣。与简单的内容推荐不同,协同过滤能发现"用户不知道但可能会喜欢"的景点,比如通过分析发现喜欢洪崖洞的用户也常对长江索道感兴趣,这种隐性关联是传统分类推荐难以捕捉的。
关键设计原则:推荐结果既要保证准确性(用户确实感兴趣),又要保持多样性(避免推荐同质化景点),同时控制计算复杂度以保证实时响应。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构设计
2.1 整体架构设计
系统采用典型的三层架构:
- 表现层:Vue 3组合式API开发,Element Plus组件库
- 业务层:Spring Boot 2.7 + MyBatis-Plus
- 数据层:MySQL 8.0主从集群 + Redis 7缓存
mermaid复制graph TD
A[用户端] -->|HTTP请求| B[Nginx]
B -->|API路由| C[Spring Cloud Gateway]
C -->|推荐请求| D[Recommend Service]
C -->|用户行为| E[Behavior Collect Service]
D --> F[Redis Cluster]
D --> G[MySQL Cluster]
E --> H[Kafka]
H --> I[Flink实时计算]
2.2 关键技术选型考量
-
Vue.js选择理由:
- 响应式编程模型适合频繁更新的推荐结果展示
- 组件化开发便于复用评分、景点卡片等UI元素
- 丰富的生态系统(Vue Router、Pinia等)
-
协同过滤算法选型:
- 用户基础协同过滤(UserCF):适合用户量大的场景
- 物品基础协同过滤(ItemCF):适合物品稳定的场景
- 最终采用混合模式:新用户用ItemCF,老用户用UserCF
-
数据库设计:
sql复制CREATE TABLE `user_behavior` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` varchar(32) NOT NULL, `spot_id` int NOT NULL, `behavior_type` tinyint COMMENT '1-浏览 2-收藏 3-评分', `rating` decimal(3,1) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_spot` (`user_id`,`spot_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. 数据工程实现
3.1 数据采集方案
我们通过三种渠道获取初始数据:
-
公开平台爬取(合规前提下):
- 使用Python+Scrapy爬取携程、美团等平台的景点数据
- 关键字段:景点名称、坐标、标签、评分、评论数
- 反爬策略:动态UserAgent+IP代理池+请求间隔控制
-
人工校验补充:
- 对爬取数据人工核对坐标准确性
- 补充官方闭园时间、特殊活动等信息
-
用户行为埋点:
javascript复制// 前端埋点示例 export const trackBehavior = (userId, spotId, eventType) => { navigator.sendBeacon('/log', JSON.stringify({ userId, spotId, eventType, timestamp: Date.now() })); };
3.2 数据预处理流程
原始数据需经过以下处理:
-
异常值处理:
- 剔除评分次数<10的景点(可信度低)
- 修正明显错误的坐标(如超出重庆范围)
-
评分标准化:
- 将各平台5分制、10分制统一转换为0-1区间
- 使用改进的Wilson区间排序算法:
python复制def wilson_score(pos, n): z = 1.96 # 95%置信度 phat = pos/n return (phat + z*z/(2*n) - z*sqrt((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n) -
特征工程:
- 景点特征向量化:[门票价格, 游玩时长, 拥挤程度, 自然风光指数...]
- 用户偏好向量化:[价格敏感度, 体力要求, 文化偏好...]
4. 推荐算法实现
4.1 协同过滤核心实现
我们实现了两种协同过滤算法:
-
用户基础协同过滤(UserCF):
java复制public List<Long> userCFRecommend(long userId, int size) { // 1. 找出相似用户 Map<Long, Double> similarUsers = findSimilarUsers(userId, 20); // 2. 加权聚合推荐物品 Map<Long, Double> candidateSpots = new HashMap<>(); for (Map.Entry<Long, Double> entry : similarUsers.entrySet()) { List<UserBehavior> behaviors = behaviorDao.findByUser(entry.getKey()); for (UserBehavior behavior : behaviors) { candidateSpots.merge(behavior.getSpotId(), entry.getValue() * behavior.getRating(), Double::sum); } } // 3. 过滤已交互物品并排序 return candidateSpots.entrySet().stream() .filter(e -> !userHasInteracted(userId, e.getKey())) .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) .limit(size) .map(Map.Entry::getKey) .collect(Collectors.toList()); } -
物品基础协同过滤(ItemCF):
- 使用改进的余弦相似度计算景点相似度
- 加入时间衰减因子:近期行为权重更高
python复制def item_similarity(train): # 计算共现矩阵 C = defaultdict(lambda: defaultdict(int)) N = defaultdict(int) for u, items in train.items(): for i in items: N[i] += 1 for j in items: if i == j: continue C[i][j] += 1 / math.log(1 + len(items)) # 计算相似度矩阵 W = defaultdict(lambda: defaultdict(float)) for i, related_items in C.items(): for j, cij in related_items.items(): W[i][j] = cij / math.sqrt(N[i] * N[j]) return W
4.2 冷启动解决方案
针对新用户和新景点问题,我们采用混合策略:
-
新用户:
- 基于人口统计学的推荐(年龄、性别等)
- 热门景点推荐(重庆Top50景点)
- 轻量级问卷调查获取初始偏好
-
新景点:
- 基于内容相似度推荐
- 探索机制:在推荐结果中混入5%新景点
实际测试表明,混合策略使新用户点击率提升37%,新景点曝光量增加2.8倍。
5. 前端工程实践
5.1 核心页面实现
-
推荐瀑布流布局:
vue复制<template> <div class="waterfall"> <div v-for="spot in spots" :key="spot.id" class="card" @click="handleClick(spot)" > <el-image :src="spot.cover" fit="cover" /> <div class="info"> <h3>{{ spot.name }}</h3> <el-rate v-model="spot.rating" disabled show-score :score-template="`${spot.rating}分`" /> </div> </div> </div> </template> <style scoped> .waterfall { column-count: 3; column-gap: 20px; } .card { break-inside: avoid; margin-bottom: 20px; } </style> -
地图联动组件:
- 集成高德地图API实现景点标注
- 点击地图标记跳转详情页
- 路径规划功能(到推荐景点的路线)
5.2 性能优化技巧
-
推荐结果缓存:
javascript复制// 使用Pinia管理推荐状态 export const useRecommendStore = defineStore('recommend', { state: () => ({ spots: [], lastUpdated: null }), actions: { async fetchRecommend() { if (this.lastUpdated && Date.now() - this.lastUpdated < 5*60*1000) { return; // 5分钟内不重复请求 } const res = await axios.get('/api/recommend'); this.spots = res.data; this.lastUpdated = Date.now(); } } }); -
图片懒加载:
html复制<el-image lazy :src="spot.cover" :preview-src-list="[spot.cover]" /> -
请求防抖处理:
javascript复制import { debounce } from 'lodash-es'; export default { methods: { search: debounce(function(keyword) { this.$axios.get('/api/search', { params: { keyword } }); }, 500) } }
6. 后端API设计
6.1 关键API实现
-
推荐接口:
java复制@RestController @RequestMapping("/api") public class RecommendController { @GetMapping("/recommend") public ResponseEntity<List<Spot>> getRecommend( @RequestHeader("X-User-ID") String userId, @RequestParam(defaultValue = "10") int size) { String cacheKey = "rec:" + userId; List<Spot> spots = redisTemplate.opsForValue().get(cacheKey); if (spots == null) { spots = recommendService.userCFRecommend(userId, size); redisTemplate.opsForValue().set( cacheKey, spots, 30, TimeUnit.MINUTES); } return ResponseEntity.ok(spots); } } -
行为收集接口:
python复制@app.route('/api/behavior', methods=['POST']) def collect_behavior(): data = request.get_json() # 验证数据格式 if not validate_behavior(data): return jsonify({"status": "error"}), 400 # 异步写入Kafka producer.send('user_behavior', value={ 'user_id': data['userId'], 'spot_id': data['spotId'], 'behavior_type': data['type'], 'timestamp': int(time.time() * 1000) }) return jsonify({"status": "success"})
6.2 安全防护措施
-
JWT鉴权:
javascript复制// 前端axios拦截器 axios.interceptors.request.use(config => { const [token](https://taotoken.net?utm_source=ai) = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); -
接口限流:
java复制@Configuration public class RateLimitConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new RateLimitInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/login"); } }
7. 部署与监控
7.1 容器化部署方案
使用Docker Compose编排服务:
yaml复制version: '3.8'
services:
frontend:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./dist:/usr/share/nginx/html
depends_on:
- backend
backend:
image: java:8-jre
command: java -jar /app/recommend.jar
volumes:
- ./target:/app
environment:
- SPRING_PROFILES_ACTIVE=prod
ports:
- "8080:8080"
redis:
image: redis:alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
redis_data:
7.2 监控体系建设
-
Prometheus监控指标:
java复制@RestController public class MetricsController { private final Counter requestCounter = Counter.build() .name("api_requests_total") .help("Total API requests.") .register(); @GetMapping("/api/metrics") public String getMetrics() { requestCounter.inc(); return "Metrics recorded"; } } -
ELK日志收集:
python复制# Logstash配置示例 input { file { path => "/var/log/recommend.log" start_position => "beginning" } } filter { grok { match => { "message" => "%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} %{GREEDYDATA:msg}" } } } output { elasticsearch { hosts => ["elasticsearch:9200"] index => "recommend-%{+YYYY.MM.dd}" } }
8. 效果评估与优化
8.1 A/B测试方案
我们设计了以下实验组:
- 对照组:基于热门的推荐
- 实验组1:纯UserCF算法
- 实验组2:混合算法(UserCF+ItemCF)
关键指标对比:
| 指标 | 对照组 | UserCF | 混合算法 |
|---|---|---|---|
| CTR | 3.2% | 5.7% | 6.9% |
| 平均停留时长 | 42s | 68s | 83s |
| 收藏转化率 | 1.1% | 2.3% | 2.8% |
8.2 持续优化方向
-
算法层面:
- 引入图神经网络捕捉高阶关系
- 尝试强化学习动态调整推荐策略
-
工程层面:
- 实现实时特征更新(Flink流处理)
- 优化相似度计算(局部敏感哈希)
-
产品层面:
- 增加推荐理由展示("因为您喜欢A,所以推荐B")
- 开发季节限定推荐(春节特别路线)
经过3个月迭代,系统推荐准确率(Precision@10)从0.32提升到0.51,用户满意度评分达到4.6/5.0。
