基于LBS和混合推荐算法的智能旅游导游系统设计与实现

发布时间:2026/9/15 18:27:12
基于LBS和混合推荐算法的智能旅游导游系统设计与实现
简介这是一份面向计算机专业本科生和毕业设计开发者的完整课程作业/毕设源码围绕LBS地理位置服务与混合推荐算法实现智能旅游导游系统。项目同时覆盖Android客户端Kotlin编写的kt界面与交互逻辑、Java后端服务及gradle构建配置并集成so动态库完成地图定位与导航能力整体结构清晰适合作为毕设框架参照和二次开发基础。压缩包共91个文件、6.47MB主要包含xml布局与配置、png图片资源、java与kt源码、gradle与jar依赖、so库及少量JS脚本前端界面、服务接口、数据存储与构建脚本均有完整呈现。目前已有173人学习下载。市场中同类毕设多为付费辅导或碎片代码本包一次集成了从定位到推荐的完整闭环附Git版本配置、IDE运行配置和README说明可帮助读者快速跑通项目理解LBS调用、推荐算法融合与Android工程组织方式节省大量搭建时间。1. 基于LBS和混合推荐算法的智能旅游导游系统到底要解决什么问题场景很具体游客到了陌生城市打开旅游应用按距离排序看到的全是周边小公园按评分排序又把 15 公里外的 5A 景区推到了最前面。协同过滤不管地理位置纯 LBS 查询又不管个人偏好。这个课题的核心就是把三者揉在一起——拿到用户 GPS 坐标结合历史行为数据用协同过滤、内容推荐和距离衰减三个维度的得分做加权融合输出一份既符合口味又顺路的景点列表再在列表上规划游览路线。对做毕设的学生它覆盖了数据建模、算法实现、接口设计和缓存优化对后端工程师这套思路可以直接迁移到本地生活、外卖、出行类产品的周边个性化推荐场景。所以阅读这份工程之前先理解它的推荐链路比看懂某一个类更有价值。2. LBS 数据建模景点经纬度怎么存、球面距离怎么算2.1 技术栈怎么搭配毕设项目最常见的选型是 Spring Boot 加 MySQL 加 Redis前端配小程序或者 Vue 加高德地图。我一般建议换成 Python FastAPI原因很实际推荐算法部分要频繁做矩阵运算和数据集切分pandas 和 numpy 的生态能让协同过滤的代码量压缩一半以上FastAPI 自带参数校验和交互式接口文档答辩演示也方便。整体结构是前端发定位后端接坐标MySQL 存 POI 和行为数据Redis 缓存推荐结果算法层单独封装成一个包不跟 Web 层耦合这样后面换算法或者调参数都不用动接口代码。2.2 景点表和用户行为表怎么设计景点表是位置服务的底座每个 POI 必须有经纬度、分类和标签。用户行为表记录浏览、收藏、评分等事件推荐算法的评分矩阵就是从这张表聚合出来的。两张表的建表语句可以直接拿去用CREATE TABLE pois ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category VARCHAR(50) NOT NULL, tags VARCHAR(255) COMMENT 逗号分隔如:历史,古建筑,门票低, lat DOUBLE NOT NULL COMMENT 纬度GCJ-02, lng DOUBLE NOT NULL COMMENT 经度GCJ-02, rating FLOAT DEFAULT 0 COMMENT 全局均分, view_count INT DEFAULT 0 COMMENT 浏览热度 ); CREATE TABLE user_behaviors ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, poi_id INT NOT NULL, behavior_type TINYINT NOT NULL COMMENT 1浏览 2收藏 3评论 4分享, score FLOAT DEFAULT 0 COMMENT 显式评分为0表示未打分, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id, created_at) );字段设计上有几个讲究。tags用逗号分隔的短字符串数据量小做内容推荐时直接按分隔符切词就行比关联表省一次 JOINbehavior_type用 TINYINT 不用 ENUM后续加行为类型不用改表结构score是显式评分浏览、收藏这类隐式行为统一按类型映射成权重分比如浏览 1 分、收藏 3 分、评论 5 分聚合出来的评分矩阵才有区分度。2.3 Haversine 公式和坐标系的坑计算两个经纬度点之间的球面距离最常用的是 Haversine 公式。我习惯在应用层算而不是指望 SQL这样算法和接口都能复用同一份实现import math def haversine(lat1: float, lng1: float, lat2: float, lng2: float) - float: R 6371.0 # 地球平均半径单位公里 phi1, phi2 math.radians(lat1), math.radians(lat2) d_phi math.radians(lat2 - lat1) d_lambda math.radians(lng2 - lng1) a math.sin(d_phi / 2) ** 2 \ math.cos(phi1) * math.cos(phi2) * math.sin(d_lambda / 2) ** 2 c 2 * math.asin(math.sqrt(a)) return R * c参数含义R取 6371.0 公里lat1/lng1是用户当前位置lat2/lng2是景点坐标返回值单位是公里。同城尺度下精度到几十米完全够用。必须提醒的坑是国内地图坐标系高德、腾讯用的是 GCJ-02火星坐标微信小程序的getLocation返回的也是加偏后的坐标而手机 GPS 裸数据是 WGS-84。如果两套坐标混存算出来的附近会整体偏移几百米推荐排序直接受影响。统一策略是入库前全部转成 GCJ-02经纬度保留 6 位小数。2.4 SQL 附近查询怎么写拿到用户经纬度后第一步是圈出候选景点。最简单的写法是直接在 SQL 里套 Haversine配合 HAVING 做半径过滤SELECT id, name, category, rating, (6371 * acos(cos(radians(%s)) * cos(radians(lat)) * cos(radians(lng) - radians(%s)) sin(radians(%s)) * sin(radians(lat)))) AS distance FROM pois HAVING distance %s ORDER BY distance;四个参数依次是用户纬度、用户经度、用户纬度、搜索半径公里。注意 HAVING 后面能直接用 SELECT 里的别名distanceWHERE 里不行。POI 数据量在万级以内时全表扫描没问题到十万级建议先加一层正方形过滤lat BETWEEN %s AND %s AND lng BETWEEN %s AND %s缩到半径附近的经纬度框再算精确距离省掉大部分 acos 计算。不同方案的适用场景可以参考这个对比距离计算方式适用数据量精度实现成本应用层 Haversine万级以下高低SQL 内联 Haversine万级以下高最低经纬度正方形预过滤十万级中先粗后精低Geohash 前缀索引十万级以上中中Redis GEO高频附近查询高中3. 混合推荐算法实现协同过滤、内容推荐、距离衰减怎么融合3.1 为什么单算法撑不住这个场景协同过滤的评分矩阵在旅游场景下极端稀疏一个普通用户一年逛过的景点也就几十个景点总量有成百上千余弦相似度算出来几乎全是零内容推荐只看标签相似容易把同质化景区反复推给用户纯 LBS 更简单永远推最近的用户口味完全不参与。混合推荐的核心逻辑不是把三个列表拼接而是把用户对景点的最终适配度拆成三个可量化的得分分别归一化之后加权求和让协同过滤负责发现口味、内容推荐负责解释理由、LBS 负责管住距离。3.2 基于用户的协同过滤皮尔逊加权打分评分矩阵规模通常不大直接用 pandas 的 DataFrame 就能跑。实现思路是找到评分行为最相似的前 K 个用户用相似度做权重加权汇总这些邻居的评分得到目标用户对每个景点的预测分import pandas as pd import numpy as np def user_cf_predict(user_id: int, rating_matrix: pd.DataFrame, k: int 30, top_n: int 10) - pd.Series: if user_id not in rating_matrix.index: return pd.Series(dtypefloat) # 新用户直接返回空 user_vec rating_matrix.loc[user_id] def pearson(row): mask user_vec.notna() row.notna() if mask.sum() 2: return 0.0 # 共同评分太少相关系数无意义 return np.corrcoef(user_vec[mask], row[mask])[0, 1] sims rating_matrix.apply(pearson, axis1) sims sims.drop(indexuser_id).sort_values(ascendingFalse).head(k) scores pd.Series(0.0, indexrating_matrix.columns) for neighbor, sim in sims.items(): scores scores.add(rating_matrix.loc[neighbor].fillna(0) * sim, fill_value0) scores scores / (sims.sum() 1e-6) # 相似度和归一化 scores scores.drop(labelsuser_vec[user_vec.notna()].index, errorsignore) return scores.sort_values(ascendingFalse).head(top_n)参数说明k是邻居数量30 在景点场景下已经够用太大容易混入低相似度噪声top_n是返回的候选数量。mask.sum() 2的过滤很关键只有一两个共同评分算出的相关系数接近 ±1 或 0没有统计意义直接当噪声丢掉。最后一行把用户已去过的景点剔除避免旧景点重复出现。3.3 基于内容的推荐标签向量余弦相似度景点很少有多维文本描述最省事的做法是用tags字段做 TF-IDF 向量算出景点之间的余弦相似度再取用户最近喜欢的几个景点把它们对应的相似度行向量累加平均。用 sklearn 几行就能完成from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity def content_predict(user_id: int, pois: pd.DataFrame, history: pd.DataFrame, top_n: int 10) - pd.Series: vec TfidfVectorizer(tokenizerlambda s: s.split(,)) tag_matrix vec.fit_transform(pois[tags].fillna()) sim_matrix cosine_similarity(tag_matrix) recent_ids history[history[user_id] user_id] \ .sort_values(created_at, ascendingFalse)[poi_id].head(3) scores np.zeros(len(pois)) for pid in recent_ids: idx pois.index[pois[id] pid][0] scores sim_matrix[idx] if len(recent_ids) 0: scores / len(recent_ids) return pd.Series(scores, indexpois[id]) \ .sort_values(ascendingFalse).head(top_n)逻辑说明tokenizer 直接把逗号分隔的标签切词top_n控制返回候选数量。加权含义是用户口味由最近三条行为代表每条行为贡献一行相似度向量取平均后就是内容维度的预测分。这里一个容易翻车的细节是标签入库时绝不能混用中英文逗号否则split(,)会把历史,古建筑和历史古建筑切成不同的词相似度直接失真。3.4 LBS 距离衰减函数与混合公式距离不能直接当得分用15 公里内的景区按线性衰减会降得太快按 1/d 又会把 200 米和 2 公里的差距放大到失真。常见做法是指数衰减def distance_decay(distance_km: float, d0: float 5.0) - float: return np.exp(-distance_km / d0)d0是衰减半径等于 d0 时得分掉到 36.8%5 公里开外的景点基本不参与竞争。三个分数的量纲完全不同融合前必须先归一化def min_max_norm(s: pd.Series) - pd.Series: span s.max() - s.min() if span 1e-9: return pd.Series(0.5, indexs.index) return (s - s.min()) / span def hybrid(user_id: int, cf_scores: pd.Series, content_scores: pd.Series, poi_distance: pd.Series, alpha0.4, beta0.3, gamma0.3) - pd.Series: norm_cf min_max_norm(cf_scores.reindex(poi_distance.index, fill_value0)) norm_cb min_max_norm(content_scores.reindex(poi_distance.index, fill_value0)) lbs poi_distance.apply(lambda d: distance_decay(d)) final_scores alpha * norm_cf beta * norm_cb gamma * lbs return final_scores.sort_values(ascendingFalse)alpha、beta、gamma的和保持为 1分别控制协同过滤、内容、位置三路权重。新用户没有行为数据时cf 和 cb 都是空reindex之后填 0此时公式自动退化成纯位置推荐这在毕设里是一个可以接受的降级策略也是后面冷启动兜底的雏形。4. 智能旅游导游后端实现推荐接口、路线规划与 Redis 缓存4.1 推荐接口的入参与返回结构接口给前端调用前端拿到定位后把经纬度传过来。返回结构里除了景点本身还要带上距离和推荐理由答辩时演示效果更直观。FastAPI 实现如下from fastapi import FastAPI, Query import pandas as pd app FastAPI() app.get(/api/recommend) def recommend(user_id: int, lat: float Query(..., descriptionGCJ-02纬度), lng: float Query(..., descriptionGCJ-02经度), top_n: int 10): pois load_pois() # DataFrame: id,name,category,tags,lat,lng,rating history load_behaviors() # DataFrame: user_id,poi_id,behavior_type,score,created_at pois[distance] pois.apply( lambda r: haversine(lat, lng, r[lat], r[lng]), axis1) candidates pois[pois[distance] 15].copy() # 先圈半径15公里 if len(candidates) top_n: candidates pois.sort_values(distance).head(50).copy() rating_matrix build_rating_matrix(history) cf_scores user_cf_predict(user_id, rating_matrix, top_n50) cb_scores content_predict(user_id, candidates, history, top_n50) merged hybrid(user_id, cf_scores, cb_scores, candidates.set_index(id)[distance]) result [] for poi_id in merged.head(top_n).index: poi candidates[candidates[id] poi_id].iloc[0] result.append({ poi_id: int(poi_id), name: poi[name], distance_km: round(poi[distance], 2), score: round(float(merged[poi_id]), 4), reason: explain_reason(cf_scores, cb_scores, poi) }) return {items: result}参数说明top_n控制最终返回条数lat/lng必须是 GCJ-02 坐标接口注释里要写清楚否则前端直接传 WGS-84 坐标时距离和排序会整体偏移。两个细节值得注意一是先按 15 公里圈候选集避免对全国几万 POI 全量算距离二是三路算法各自先取前 50 再融合而不是每条路只取 10 个因为三路高分集合的交集可能很小取太少会导致融合后列表缺项。explain_reason可以返回距你 3.2 公里 / 风格接近你收藏的 XX 美术馆这类文案是答辩演示的加分项。4.2 导游路线规划最近邻贪婪算法推荐列表确定后规划路线本质上是一个从用户当前位置出发的路径排序问题。景点数量在 20 个以内用最近邻贪婪算法就够正确性和答辩讲解都简单def plan_route(start: tuple, pois: list, stay_hours: int 2, max_hours: int 8) - list: route [] remaining pois.copy() current start spent 0.0 speed_kmh 15.0 # 公交加步行的混合速度估算 while remaining and spent max_hours: nearest min(remaining, keylambda p: haversine(current[0], current[1], p[lat], p[lng])) travel haversine(current[0], current[1], nearest[lat], nearest[lng]) / speed_kmh if spent travel stay_hours max_hours: break # 时间预算不够放弃剩余景点 route.append(nearest) spent travel stay_hours remaining.remove(nearest) current (nearest[lat], nearest[lng]) return routestay_hours是每个景点的停留时间max_hours是一天的游览总预算speed_kmh按 15 公里/小时估算接口文档里要注明这是估算行程而不是真实导航时间。每次选离当前位置最近且时间预算内能完成的景点走完就更新当前位置。想要效果更好可以换成 2-opt 局部优化遍历两两交换看能否缩短总路程但毕设论文里讲清楚最近邻加时间预算就已经足够完整。4.3 Redis 缓存与更新策略推荐结果每次重算都要全量加载行为表每一次请求都算响应时间很难看。常见做法是把推荐结果按用户和位置双重维度缓存# 推荐结果缓存 key 设计geohash5 约等于5公里见方的格子 SET rec:{user_id}:{geohash5} {items: [...]} EX 1800 # 用户产生新的收藏/评论行为后删除对应缓存 DEL rec:{user_id}:{geohash5}EX 1800表示过期时间是 30 分钟用户移动不超过一个 geohash 格子时可以复用缓存。数据更新采用定时任务每 5 分钟从 MySQL 拉增量行为重算热门景点的推荐分而不是每次请求都触发全量重算。对于毕设量级的数据这个策略已经够用答辩时能说清楚缓存 key 为什么带 geohash 前缀比笼统说用了 Redis更有说服力。5. 智能旅游导游系统验收之前的三道安检冷启动、离线评估、权重调参5.1 新用户冷启动的兜底逻辑答辩最容易露馅的场景是评委注册一个新账号发现推荐列表是空的。冷启动兜底策略要写进代码而不是口头解释。新用户没有行为数据user_cf_predict和content_predict都返回空此时推荐列表应该退化为附近加热门def cold_start_recommend(pois: pd.DataFrame, gamma: float 0.6) - pd.Series: popularity pois[rating] 0.1 * pois[view_count] location pois[distance].apply(lambda d: np.exp(-d / 5.0)) return (popularity * gamma location * (1 - gamma)) \ .sort_values(ascendingFalse)gamma控制热度与位置的权重热度分和位置分都必须先归一化到 [0,1] 再加权否则 rating 的 3 到 5 分的差值会把距离差异完全淹没。验证时用一个从未登录的 user_id 调接口确认返回的是 5 公里以内的高分景点。5.2 离线评估指标怎么算推荐效果不能只靠看起来合理。把行为数据按用户切分80% 做训练集20% 做测试集用测试集里真实收藏或评分过的景点作为 ground truth计算下面几个指标指标计算方式说明PrecisionN推荐列表命中测试集的数量 / N推荐精确度RecallN推荐列表命中测试集的数量 / 测试集景点总数召回能力Coverage被推荐到的景点数 / 景点总数结果多样性平均推荐距离推荐结果与用户定位距离的均值LBS 约束效果如果 PrecisionN 一直很低先检查评分矩阵是否太稀疏导致相似度全为 0如果平均推荐距离超过 10 公里则是distance_decay的d0调得太大或者缓存 key 没有按位置区分。5.3 权重调参的手工验证手法alpha、beta、gamma三路权重和d0衰减半径建议做一次简单的网格搜索选 Precision20 最高的一组。还有一个容易被忽视的检验构造一个专门收藏美术馆的用户作为测试账号如果推荐列表里美术馆占比没有明显高于普通用户说明content_predict的标签向量有问题多半是标签切词失败或历史行为排序方向反了。手动验证时始终用新老两个账号对比新账号看冷启动兜底、老账号看个性化差异这两个用例能覆盖系统里百分之八十的隐患。本文还有配套的精品资源点击获取