ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

微信小程序+推荐系统:遵义旅游景点推荐系统毕设全解析

微信小程序+推荐系统:遵义旅游景点推荐系统毕设全解析 每年到毕业季微信小程序加推荐系统这个组合就会在选题列表里刷屏。你可能在知乎、CSDN、B站上都刷到过“基于微信小程序的旅游景点推荐系统”点进去不是只有前端页面就是算法写成一坨根本跑不动能带源码打包好的更是少数。这篇就把我做遵义市旅游景点推荐系统这套毕设的完整过程拆开讲从需求分析、技术选型、推荐算法落地、小程序端实现到真机调试排坑全给你捋一遍。这个项目适合两类人看一是正在做类似毕设、想找个靠谱技术路线的学生二是想从零搭建一个带推荐逻辑的小程序应用、但不想看那种“只讲概念不给代码”的伪教程的开发者。我尽量把每一步的“为什么这么做”也说清楚光给结论不给思路等于白写。1. 项目整体设计与技术选型思路1.1 这个选题凭什么能做、难点在哪旅游推荐系统这个选题被选烂了但它确实是一道好题——前有景区数据可抓、中有推荐算法可写、后有微信小程序做载体天然自带一套完整的前后端闭环。比起“校园跑腿”“在线点餐”这种纯业务CRUD推荐系统类题目在答辩时更好展开你可以被问到“为什么选协同过滤”“冷启动怎么解决”“数据稀疏怎么办”这些问题都有成熟的学术回答空间不会像“订单表为什么这么设计”那样容易卡壳。说回来选遵义作为场景不是随手挑的。遵义这座城市在旅游数据上很有层次感红色旅游遵义会议会址、娄山关、世界自然遗产赤水丹霞、世界文化遗产海龙屯、酒文化体验茅台镇、生态度假乌江寨、竹海这种标签多样性对推荐系统的特征工程非常友好。如果你做的城市只有一个“古城”“海边”的标签算法实验根本拉不开差距。这套系统的完整链路是用户通过微信小程序浏览景点、搜索关键词、查看详情、收藏和评论前端把行为埋点数据上报给后端后端汇总到数据库里推荐引擎根据用户历史行为和个人标签计算出候选景点列表再通过接口返回给小程序渲染。说白了三件事数据采集、行为建模、推荐分发。1.2 技术选型原生小程序还是 uni-app后端选什么我在做技术选型的时候纠结了挺久先说结论小程序端用原生、后端用Python Flask、数据库用MySQL、推荐算法部分自己写Python脚本。没选uni-app不是因为不好而是这个项目我对多端投放没有需求。uni-app的优势是“一套代码跑三端”但代价是很多原生API要包一层一旦遇到地图组件、定位权限、导航栏这类强依赖微信能力的模块调试成本和坑位数量反而会上升。原生微信小程序学习曲线比较平缓WXML和WXSS就是类似HTML和CSS的写法逻辑层用JavaScript你只要会Flex布局和基本的ES6语法UI这块很快就能上手。如果你要做的是毕业设计一个最稳妥的思路就是原生小程序写界面后端用Python系框架写接口数据库用MySQL这样每一层都是你答辩时可以清晰讲出来的东西。后端为什么用Flask而不是Django因为Flask的轻量非常适合这种接口数量在20个左右的项目。Django自带Admin后台、ORM、模板引擎这些对纯接口型项目来说确实有大炮打蚊子的感觉。Flask的灵活性也方便后期集成推荐算法——我可以直接新建一个recommend.py在里面写协同过滤、基于内容的推荐然后在路由里调用不用额外学一套框架规范。实际跑下来Flask开发这套系统的接口速度在两天左右Django预估要三天半。2. 推荐系统核心算法与落地细节2.1 推荐系统不是玄学先想清楚“推什么”和“怎么推”很多毕设项目把推荐系统做成“热门景点排行榜”这不叫推荐叫统计。你做推荐的目的是让不同用户看到不一样的景点那就必须定义清楚用户在什么状态下需要什么推荐结果。我给这套系统设计了三种推荐场景冷启动场景新用户第一次进入小程序没有任何行为数据这时候系统没办法猜你的偏好最稳的策略是“城市热门高分口碑”组合把遵义市收藏量最高的前六个景点和评分最高的两个冷门小众景点混合推出去。基于内容的推荐当用户在详情页浏览、搜索、收藏了一些景点后系统可以分析这些景点的标签找出同类目或者强关联类目的其他景点。比如用户收藏了遵义会议会址他的行为标签里“红色旅游”权重就会变高系统下次优先推荐娄山关、红军山这类同标签景点。协同过滤推荐当平台积累了一批用户行为数据时就能用“和你行为相似的人喜欢什么”来推荐。这种推荐方式不关心景点本身是什么标签只看群体行为规律好处是能发现用户自己都没意识到的偏好比如某用户看了茅台镇系统发现“看过茅台镇的人还去逛了土城古镇”这种跨类目推荐如果纯靠人工标注标签根本做不出来。推荐系统说穿了就是特征加模型加反馈把用户和物品映射成可用数据描述的向量用各种方式算向量之间的距离再用用户反馈不断修正。2.2 协同过滤算法的Python实现我这里给出一版可以直接跑通的基于用户的协同过滤User CF代码核心逻辑就是计算用户之间的相似度找到和你口味最像的人群把他们喜欢而你没看过的景点推荐给你。import math from collections import defaultdict def load_user_behavior_data(): 模拟用户-景点行为数据 结构{用户id: {景点id: 行为权重}} 行为权重浏览1分搜索2分收藏3分评论4分 return { u001: {s001: 3, s002: 2, s003: 4, s005: 1}, u002: {s001: 2, s003: 3, s004: 4}, u003: {s001: 4, s002: 1, s004: 2, s006: 3}, u004: {s002: 3, s003: 2, s005: 4, s006: 1}, } def calculate_user_similarity(user_data): 计算用户之间的皮尔逊相关系数 这里简化成余弦相似度 user_sim defaultdict(dict) users list(user_data.keys()) for i in range(len(users)): for j in range(i 1, len(users)): user_i, user_j users[i], users[j] items_i, items_j set(user_data[user_i].keys()), set(user_data[user_j].keys()) common_items items_i items_j if not common_items: user_sim[user_i][user_j] 0 user_sim[user_j][user_i] 0 continue # 取共同评分向量 vec_i [user_data[user_i][item] for item in common_items] vec_j [user_data[user_j][item] for item in common_items] dot sum(a * b for a, b in zip(vec_i, vec_j)) norm_i math.sqrt(sum(a * a for a in vec_i)) norm_j math.sqrt(sum(b * b for b in vec_j)) if norm_i 0 or norm_j 0: sim 0 else: sim dot / (norm_i * norm_j) user_sim[user_i][user_j] sim user_sim[user_j][user_i] sim return user_sim def recommend_by_user_cf(user_id, user_data, user_sim, top_n4): 为指定用户推荐top_n个景点 if user_id not in user_data: return 查无此人 # 找出最相似的K个用户 sim_scores sorted(user_sim[user_id].items(), keylambda x: x[1], reverseTrue) similar_users [u for u, s in sim_scores if s 0][:3] # 候选物品加权评分 candidate_score defaultdict(float) candidate_reason defaultdict(list) target_items set(user_data[user_id].keys()) for other_user in similar_users: sim_score user_sim[user_id][other_user] for item, score in user_data[other_user].items(): if item in target_items: continue candidate_score[item] sim_score * score candidate_reason[item].append({ user: other_user, sim: round(sim_score, 4), score: score }) ranked sorted(candidate_score.items(), keylambda x: x[1], reverseTrue) return ranked[:top_n]这段代码逻辑很直白先构造用户行为矩阵然后算用户之间的相似度找一个“和你最像的用户群体”再把这个群体里评分高但你还没看过的东西推给你。实际项目中你要把load_user_behavior_data改成从MySQL读数据结构为user_id, item_id, weight的明细表然后用SQL聚合出用户行为矩阵。2.3 基于内容推荐的标签体系和特征权重协同过滤有冷启动问题新景点没有用户行为数据就永远推不出去。所以我同时做了基于内容的推荐它的逻辑是给每个景点打上内容标签用户对一个景点感兴趣就相当于对这些标签加权然后去找拥有相似标签的景点。标签体系设计是整个推荐系统的地基。我整理遵义景点时把特征分成四组文化历史类红色旅游、土司文化、夜郎文化、盐运文化自然风光类丹霞地貌、竹海、瀑布、峡谷、喀斯特主题体验类酒文化、美食、民宿、漂流、摄影基础属性类距离市区远近、建议游玩时长、人均消费区间拿赤水丹霞举例标签就是“自然风光-丹霞地貌”“摄影”“徒步”“世界遗产”茅台镇则是“酒文化”“美食”“夜游”“古镇”。用户收藏或长时间浏览一个景点就把这些标签的权重累加到用户画像上。比如用户看了三次茅台镇那“酒文化”权重值就涨三分下次系统会优先推标签包含“酒文化”的其他景点。def content_based_recommend(user_tags, spot_tags_list): user_tags: {红色旅游: 3, 丹霞地貌: 1} spot_tags_list: [{name: 娄山关, tags: {红色旅游: 1, 自然风光: 1}}, ...] scored [] for spot in spot_tags_list: score 0 reason [] for tag, weight in user_tags.items(): if tag in spot[tags]: score weight * spot[tags][tag] reason.append(tag) if score 0: scored.append({name: spot[name], score: score, hit_tags: reason}) scored.sort(keylambda x: x[score], reverseTrue) return scored这套混合策略我建议你直接照搬冷启动用户走热门榜有少量行为的用户走基于内容推荐等用户行为够丰富后再混合协同过滤结果按7比3加权排序。不要只做一种算法答辩时老师几乎必问“你如何解决冷启动”和“多种算法如何融合”这两种算法的结合正好把这两个问题都回答掉。3. 小程序端核心功能模块实现3.1 页面架构与底部导航设计整个小程序一共分五个一级页面首页、景点列表、搜索、个人中心、景点详情。TabBar底部导航放四个模块首页、景点、搜索、我的景点详情页不进入TabBar通过wx.navigateTo跳转这是微信小程序的标准设计习惯——需要返回上一级的页面不要放在TabBar里。首页设计的核心是推荐流的展示我采用“顶部轮播图金刚区快捷入口推荐景点瀑布流”三段式结构。轮播图放遵义几个高热度景点的高清图金刚区放“红色旅游”“自然风光”“酒文化”“古镇村落”四个分类入口点击直接跳转到对应标签的筛选列表。下面就是推荐流的核心区域每次加载8个景点卡片上拉触底后继续追加下一组用onReachBottom这个页面生命周期钩子触发分页加载。景点卡片的信息密度要足够主图、名称、评分、标签摘要、距当前位置的距离。用户从卡片上就能做初步筛选不用点进去才发现是两小时车程外的景点这种体验细节决定了应用真实使用时的留存率。3.2 导航栏高度适配与自定义头部微信小程序的顶部导航栏有个历史遗留问题不同机型、不同系统版本下胶囊按钮的位置和状态栏高度不一样如果你用自定义导航栏样式比如想在导航栏放搜索框直接写死padding-top就会在部分机型上出bug。我的做法是在app.js的onLaunch里用wx.getMenuButtonBoundingClientRect()拿胶囊按钮的坐标信息再结合wx.getSystemInfoSync()拿状态栏高度算出导航栏实际高度后放进全局变量。App({ onLaunch() { const menuButton wx.getMenuButtonBoundingClientRect(); const systemInfo wx.getSystemInfoSync(); this.globalData.statusBarHeight systemInfo.statusBarHeight; this.globalData.navBarHeight (menuButton.top - systemInfo.statusBarHeight) * 2 menuButton.height; this.globalData.menuButton menuButton; } })这样在自定义导航栏的页面里直接取app.globalData.navBarHeight作为padding-top即可。这个方案实测在iPhone 12、小米11、华为P40等不同机型上都能对齐胶囊按钮不会出现搜索框被刘海顶走或者和胶囊重叠的情况。3.3 地图与定位让附近景点推荐真正落地旅游类小程序不接地图组件就太可惜了。我在地图模块用了map组件页面加载时先调用wx.getLocation获取用户当前经纬度再把遵义市范围内的景点坐标标注到地图上。用户点击标记点弹出景点名称和简要介绍卡片点击卡片跳转详情页。这里有个权限和合规上的细节要提醒从基础库2.9.0开始wx.getLocation需要在app.json里声明permission字段并填写用途说明否则调用会直接失败。{ permission: { scope.userLocation: { desc: 你的位置信息将用于景点就近推荐和导航 } } }定位成功后前端会在景点列表里显示“距你xx公里”这个距离是通过后端接口实时计算的。我这里没有直接用前端算因为景点数据量大几十上百个景点全拉到前端再算距离会拖慢渲染接口里用SQL的ST_Distance_Sphere做完距离排序再返回性能差距非常明显。列表页也支持“按距离优先”和“按评分优先”排序这两个排序字段在MySQL里都建了索引。刚开始我没加索引数据到三千条的时候排序接口已经掉到600多毫秒加了联合索引后直接压到80毫秒以内这种细节优化在答辩时提出来会很加分。4. 从开发到上线的完整实操流程4.1 项目初始化与数据库设计第一步先在微信公众平台注册小程序账号拿到AppID然后下载微信开发者工具新建项目时选择“不使用云服务”和“JavaScript基础模板”。数据库设计这块我建议你在一开始就多花点时间因为后面接推荐算法、接管理后台都要围绕表结构来。我的数据库一共7张核心表user微信用户信息表字段有openid、nickname、avatar_url、created_atspot景点主表包含name、summary、cover_url、lat、lng、score、city_id、tag_idstag标签表就是上文的推荐标签字典user_behavior行为埋点明细表每条记录代表用户一次浏览/搜索/收藏/评论操作带behavior_type字段favorite用户收藏关系表comment景点评论表search_log搜索关键词记录表user_behavior表是推荐算法的数据源它的设计直接决定后面算法好不好写。我选择的是“明细流水”而非聚合表因为算法推荐需要还原整个行为序列而不是只看一个汇总值。每次埋点都实时插入一条流水定期跑离线脚本聚合成用户特征。4.2 小程序登录授权与接口联调微信小程序的登录体系比其他Web应用复杂一些但套路是固定的小程序端通过wx.login拿到临时code把code发给自己的后端后端拿到这个code再调用微信的接口换取openid和session_key然后用自己的逻辑生成一个自定义登录态返回给前端后续使用。wx.login({ success: async (res) { const { code } res; const loginRes await request(/api/user/login, { method: POST, data: { code } }); wx.setStorageSync(token, loginRes.data.token); wx.setStorageSync(userInfo, loginRes.data.userInfo); } })后端Flask处理这个code时用requests库请求微信接口获取openid然后查数据库没注册过就先插入一条新用户记录再发放一个自己签名的JWT token返回前端。以后所有带推荐逻辑的接口都要求请求头里带上Authorization: Bearer token后端用装饰器做登录校验。这一步不要用wx.getUserProfile里的数据当用户身份code换openid的服务端逻辑才是微信登录的官方正解。4.3 发布前的真机预览与上线部署开发完不等于能上线微信小程序上线前有个关键步骤把后端接口域名配到小程序后台的“开发管理-服务器域名”里而且必须是HTTPS协议。很多人在本地用http://localhost:5000调接口调得顺风顺水一上真机就全部请求失败报错信息request:fail url not in domain list就是因为没配置合法域名。如果只是毕业设计演示不打算正式发布到微信平台审核可以在开发者工具右上角“详情-本地设置”里勾选“不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”真机预览时也要在手机端打开“开发调试”模式才不校验域名。这套演示方案在答辩现场基本够用但如果你想让作品长期可访问还是老老实实买域名、备案、配HTTPS证书。发布完整流程我走了一遍开发者工具点击“上传”在公众平台后台的“版本管理”里找到开发版本提交审核审核通过后点“发布”。个人主体小程序不建议涉及旅游信息展示和评论这类类目审核时大概率要求提供资质证明这时候要么换企业主体账号要么把实战项目做成非上线的体验版演示。5. 常见问题与调试排坑实录5.1 列表数据渲染异常第一个高频坑是图片空白。景点卡片里的图片如果用的是本地相对路径真机上会加载失败必须用线上HTTPS图片地址或者将图片转为Base64格式嵌入。开发工具里正常不代表真机上正常这是我在这个项目里踩得最深的一个坑。第二个高频坑是分页加载时数据重复。原因通常是onReachBottom触发了多次网络请求而上一页的请求还没返回两批数据拼到了一起。解决办法是加一个isLoading锁请求没结束前不发起新请求同时对已渲染的景点id做去重。onReachBottom() { if (this.data.isLoading) return; this.setData({ isLoading: true }); this.loadMoreSpots().finally(() { this.setData({ isLoading: false }); }); }5.2 推荐结果不准的排查思路如果你辛辛苦苦写完协同过滤跑出来却觉得推荐结果不合理先别急着怀疑算法按顺序检查三件事埋点数据是否真的收集上来了、用户行为权重是否区分了浏览和收藏、标签体系是否存在语义重叠。我一开始给所有行为类型设置的权重都是1结果发现用户一不小心浏览了几个“热门景点”详情页最后推荐结果和热门榜几乎没区别。后来改成浏览1分、搜索2分、收藏3分、评论4分推荐效果才有区分度。另一个问题是标签“古镇”和“夜景”被重复打在某些景点上导致基于内容的推荐总是返回同一批景点后来给标签加了唯一性约束才解决。5.3 答辩时的高频追问与应对思路做毕设绕不开答辩我把自己被问到的问题和参考回答整理一下你按这个思路去准备基本稳第一个问题“为什么用户行为权重设置为1、2、3、4而不是其他值”参考思路这四个行为代表用户由低到高的意向强度浏览可能是误触收藏和评论一定是主动操作。具体数值确实有主观因素但模型上线后可以根据用户点击反馈去调参比如推荐列表里被用户点击的景点提升对应行为维度的权重。第二个问题“如果景点数据量到十万级这个推荐算法还能用吗”参考思路基础的User CF计算用户相似度是O(n^2)复杂度十万用户时确实扛不住需要引入离线预计算和倒排索引先把相似用户算好存Redis在线推荐阶段只做查询不做实时计算。毕设场景下可以提前说明这套优化思路属于加分项。第三个问题“如何评估推荐效果”参考思路可以引入离线评测指标precisionk和recallk或者简单一点统计推荐位点击率。我在项目里做了一个最简单的评估把一部分用户历史行为当测试集用剩下的行为当训练集看模型能不能预测中用户真正点击过哪些景点按准确率做了三组实验对比。6. 我个人实操中的几点体会做完这个项目最大的感受是毕设题目的好坏不在题目本身而在你对题目的拆解深度。微信小程序旅游推荐系统这套组合被选了无数次但真正拉开差距的是你在推荐策略上做了什么思考、在工程化落地时避开了哪些坑。我建议你按照这个顺序推进项目先把景点数据做好标签化再把Flask接口写完然后用小程序把这些接口串起来最后再加推荐算法。顺序反过来的话你会发现算法做得再好没有数据支撑、没有前台展示一切都是空中楼阁。这套流程我自己跑下来从零到一大概用了三周如果只做核心功能不加多余的过渡动画两周半也足够了。另外如果你不打算只把这个项目当成一个“交差”的毕设我特别推荐你再扩展一个管理后台用Flask的Admin模块或者简单的Vue页面都行能维护景点数据和查看用户行为报表。答辩时能展示出后台管理能力整个项目的完整度会直接高一个档次。源码部分我已经完整打包整理涉及的技术点都加了注释拿到后按README里的步骤配置环境就能本地跑通有问题也欢迎直接交流。
返回列表