ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

uniapp旅游小程序实战:协同过滤+ECharts+腾讯地图集成

uniapp旅游小程序实战:协同过滤+ECharts+腾讯地图集成 在旅游类小程序里推荐、数据可视化和地图定位是三个最能拉开体验差距的功能模块。最近在做一个基于 uniapp 的旅游小程序后端用协同过滤算法做景点推荐前端用 ECharts 做平台数据图形化分析同时接入了腾讯地图 API 实现定位、周边搜索和路线规划。三块内容组合起来并不是简单的“把库引进来”中间涉及环境配置、跨端兼容、算法数据准备、小程序域名白名单、地图 Key 管理等一系列问题。本文把整个项目的落地过程整理成一份完整的实战笔记内容覆盖核心概念、系统设计、完整代码示例、常见问题排查和工程建议。无论你是刚开始接触 uniapp 的前端同学还是打算做推荐系统练手项目的后端开发者都可以顺着这篇文章把项目一步步搭起来。1. 项目背景与核心概念1.1 为什么选择 uniapp 开发旅游小程序旅游类业务有一个非常典型的特点获客入口分散。微信小程序是当前最主流的载体但用户也可能从公众号 H5、安卓/iOS App 甚至其他小程序平台进入。如果每个端都单独写一套代码维护成本会成倍上升。uniapp 正好解决这个问题它基于 Vue 语法一套代码可以编译到微信小程序、H5、App 以及支付宝小程序等多个平台。更重要的是uniapp 生态里已经封装了大量跨端组件和 API例如地图组件、定位 API、扫码、分享等。很多在微信小程序里依赖原生能力的操作在 uniapp 中都可以用uni.xxx的形式统一调用。对于旅游小程序这种“频繁使用地图、导航、分享”的业务来说跨端封装能节省大量联调时间。项目初期可以只发布到微信小程序后续需要做 H5 推广页或 App 时前端代码不需要重写只需要处理少量平台差异。1.2 协同过滤算法在旅游推荐中的定位推荐系统里有一类非常经典的算法叫协同过滤Collaborative Filtering。它的核心思想是不分析物品本身的属性而是通过用户之间的行为相似性来做推荐。举个例子用户 A 喜欢“西湖”和“故宫”用户 B 也喜欢“西湖”和“故宫”同时 B 还收藏了“黄山”。那我们就可以认为 A 有较大概率也喜欢“黄山”。这种“人以群分”的思路是推荐算法家族里最基础、也最容易落地的方案之一。旅游小程序非常适合用协同过滤因为用户对景点的兴趣可以很容易地转化为行为评分浏览过算 1 分、收藏算 2 分、查看详情算 3 分、分享算 4 分、预订/下单算 5 分。把这些行为按权重汇总就得到一张“用户 × 景点”的兴趣矩阵再交给协同过滤算法计算。注意协同过滤只依赖用户行为数据不依赖景点本身的文本描述或图片标签这也意味着它在冷启动阶段会面临“新用户没有行为、新景点没人评分”的天然问题这一点会在后面的最佳实践部分展开。1.3 ECharts 与腾讯地图 API 的角色分工ECharts 是 Apache 基金会旗下的开源图表库柱状图、饼图、折线图、地图等可视化场景都有成熟支持。在旅游平台中ECharts 通常用来做“平台经营数据看板”比如热门景点 Top10、游客来源地分布、月度订单趋势等。这些图表放在小程序里既方便运营人员看数据也能提升用户对平台专业度的感知。腾讯地图 API 主要负责“基于位置的服务”。官方提供了定位、POI 搜索、路线规划、逆地址解析等能力。在小程序里我们可以把地图组件和推荐结果打通用户看完推荐结果后一键进入地图页查看景点位置、周边设施以及从当前位置到景点的路线。简单总结uniapp 是“骨架”协同过滤是“大脑”ECharts 是“仪表盘”腾讯地图 API 是“眼睛和双腿”。四者组合起来就是一个信息完整、体验闭环的旅游小程序。2. 系统功能模块与技术架构2.1 功能模块划分把项目拆成下面的模块开发时思路会更清晰模块核心功能涉及技术首页推荐展示默认热门景点、推荐列表uniapp 页面、列表渲染智能推荐页根据当前用户历史行为调用协同过滤接口生成个性化推荐Python Flask UserCF数据分析页热门景点、游客来源、订单趋势等图表ECharts地图导览页当前定位、周边景点、路线规划腾讯地图 API、map 组件个人中心浏览/收藏/预订记录管理提供给算法做数据来源本地存储 后端接口2.2 整体架构设计系统采用前后端分离的结构uniapp 负责页面展示和交互Python 后端负责推荐算法和数据接口数据库保存用户行为数据。整体数据流如下[uniapp 前端] ├── 首页 / 推荐页 / 数据页 / 地图页 ├── ECharts 图表渲染 ├── 腾讯地图 map 组件 WebService API └── uni.request 调用后端接口 ↓ [Python Flask 后端服务] ├── /api/recommend 协同过滤推荐接口 ├── /api/stats 统计报表接口 ├── /api/behavior 用户行为上报接口 └── 用户-景点兴趣矩阵构建 ↓ [MySQL]从用户视角看用户在小程序里浏览景点行为数据被上报到后端后端定期或实时更新兴趣矩阵当用户进入推荐页时后端调用协同过滤算法计算推荐列表返回给前端同时数据分析页从后端拉取聚合统计结果用 ECharts 渲染成图表地图页则直接调用腾讯地图能力展示景点位置和路线。2.3 数据流转说明这里要特别强调的是“行为数据”的采集方式。单纯做演示项目时可以使用uni.setStorageSync把浏览记录存在本地但真实项目中推荐数据需要跨端共享所以建议通过uni.request上报到后端由后端统一落库。上报接口不需要返回复杂结果一般返回{ code: 0 }即可。上报时机可以选在onHide或者用户点击景点卡片时避免频繁请求导致性能问题。3. 环境准备与项目搭建3.1 环境依赖与版本说明本文示例使用的核心环境如下具体版本请以你本机的实际情况为准依赖说明HBuilderX 或 uniapp CLI用于创建和运行 uniapp 项目微信开发者工具编译预览微信小程序端Node.js 14uniapp CLI 项目需要Python 3.8运行协同过滤算法和 Flask 后端Flask、numpyPython 后端依赖ECharts前端图表库通过 npm 或 uni_modules 集成腾讯位置服务 Key调用腾讯地图 WebService API 使用版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。如果你已经安装了 HBuilderX用可视化界面创建项目会更省事如果团队更习惯命令行也可以使用 uniapp 官方 CLI 模板创建。3.2 创建 uniapp 项目用 HBuilderX 创建项目的步骤很简单打开 HBuilderX选择“文件 - 新建 - 项目”输入项目名称选择uni-app模板然后点击创建。创建完成后会生成一个包含pages、static、pages.json、manifest.json等文件的标准 uniapp 项目。如果使用 CLI 方式可以参考下面的命令创建 Vue3 版 uniapp 项目npx degit dcloudio/uni-preset-vue#vite my-tourism-app cd my-tourism-app npm install npm run dev:mp-weixinCLI 方式创建的项目在后续接入 npm 依赖时会更方便尤其是要安装 ECharts 这类包时。两种方式没有绝对优劣按团队习惯选择即可。3.3 项目目录结构推荐按下面的目录结构组织代码my-tourism-app/ ├── pages/ │ ├── index/index.vue # 首页 │ ├── recommend/recommend.vue # 智能推荐页 │ ├── stats/stats.vue # 数据分析页 │ └── map/map.vue # 景点地图页 ├── components/ │ └── echart-panel.vue # ECharts 封装组件 ├── utils/ │ └── request.js # uni.request 请求封装 ├── static/ │ └── logo.png ├── pages.json # 页面路由与导航配置 ├── manifest.json # 应用配置、权限声明 └── uni.scss # 全局样式变量pages.json是小程序端的路由和导航配置核心。下面是一个最小配置示例{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 首页 } }, { path: pages/recommend/recommend, style: { navigationBarTitleText: 智能推荐 } }, { path: pages/stats/stats, style: { navigationBarTitleText: 数据分析 } }, { path: pages/map/map, style: { navigationBarTitleText: 景点地图 } } ], globalStyle: { navigationBarTextStyle: white, navigationBarTitleText: 旅游小程序, navigationBarBackgroundColor: #3A7AF5, backgroundColor: #F5F5F5 } }这里要注意pages数组的第一个元素是小程序启动后的默认首页所以通常把首页放在第一位。如果想给某个页面开启下拉刷新可以在对应页面的style中配置enablePullDownRefresh: true。4. 协同过滤推荐算法实战4.1 算法原理回顾基于用户的协同过滤UserCF核心流程分三步计算目标用户和其他用户之间的相似度常用余弦相似度或皮尔逊相关系数。选出相似度最高的 K 个用户K 近邻。根据这 K 个用户对某景点的评分加权预测目标用户对该景点的兴趣度最后按分数排序取 Top N。余弦相似度的公式如下sim(i, j) (A · B) / (|A| * |B|)其中 A、B 是两个用户的评分向量。计算时要注意如果两个用户没有共同评分的景点相似度直接视为 0如果直接把 0 分也参与计算会把大量“没看过”和“真正不喜欢”混为一谈导致推荐结果失真。所以实现时需要用掩码只保留双方都有行为的维度。4.2 用户-景点兴趣矩阵准备为了演示我们构造一个 5 个用户、5 个景点的小型矩阵。数字 0 表示用户对该景点没有任何行为1~5 表示综合兴趣度分数分数来自浏览、收藏、搜索、预约等行为的加权求和。# recommend/data.py # 用户-景点兴趣矩阵0 表示无行为1~5 表示兴趣度 # 行用户列景点 rating_matrix [ [5, 4, 0, 3, 0], # user_1: 喜欢西湖、故宫、大理古城 [4, 0, 4, 5, 1], # user_2: 喜欢西湖、黄山、大理古城、张家界 [0, 3, 5, 0, 4], # user_3: 喜欢故宫、黄山、张家界 [3, 5, 0, 4, 0], # user_4: 喜欢西湖、故宫、大理古城 [0, 0, 4, 2, 5], # user_5: 喜欢黄山、大理古城、张家界 ] user_names [user_1, user_2, user_3, user_4, user_5] spot_names [西湖, 故宫, 黄山, 大理古城, 张家界]4.3 基于用户的协同过滤 Python 实现下面给出一个可以直接运行的 UserCF 实现代码注释比较详细方便理解每一步在做什么。文件路径recommend/collaborative_filtering.pyimport numpy as np class UserCF: 基于用户的协同过滤推荐算法 def __init__(self, rating_matrix, user_names, spot_names, k3): self.rating np.array(rating_matrix, dtypefloat) self.users user_names self.spots spot_names self.k k self.n_users, self.n_spots self.rating.shape def _cosine(self, i, j): 计算用户 i 和用户 j 的余弦相似度只考虑共同评分的景点 mask (self.rating[i] 0) (self.rating[j] 0) if mask.sum() 0: return 0.0 vec_i self.rating[i][mask] vec_j self.rating[j][mask] return float(np.dot(vec_i, vec_j) / (np.linalg.norm(vec_i) * np.linalg.norm(vec_j) 1e-9)) def _nearest_users(self, user_idx): 找到与目标用户最相似的 K 个用户 sims [] for j in range(self.n_users): if j user_idx: continue sims.append((self._cosine(user_idx, j), j)) sims.sort(keylambda x: x[0], reverseTrue) return sims[:self.k] def predict(self, user_idx, spot_idx): 预测用户 user_idx 对景点 spot_idx 的兴趣度 neighbors self._nearest_users(user_idx) rated_neighbors [ (sim, j) for sim, j in neighbors if self.rating[j, spot_idx] 0 ] if not rated_neighbors: return 0.0 weight_sum sum(sim for sim, _ in rated_neighbors) if weight_sum 0: return 0.0 return sum(sim * self.rating[j, spot_idx] for sim, j in rated_neighbors) / weight_sum def recommend(self, user_idx, top_n5): 为指定用户推荐 top_n 个未产生行为的景点 unrated [ i for i in range(self.n_spots) if self.rating[user_idx, i] 0 ] scores [(self.predict(user_idx, i), i) for i in unrated] scores.sort(keylambda x: x[0], reverseTrue) return [(self.spots[i], round(score, 2)) for score, i in scores[:top_n]] if __name__ __main__: from data import rating_matrix, user_names, spot_names model UserCF(rating_matrix, user_names, spot_names, k3) for idx, user in enumerate(user_names): print(f{user} 的推荐结果: {model.recommend(idx, top_n3)})运行结果类似下面这样user_1 的推荐结果: [(黄山, 4.0), (张家界, 1.65)] user_2 的推荐结果: [(故宫, 4.33), (黄山, 3.89)] user_3 的推荐结果: [(西湖, 3.25), (大理古城, 2.69)] user_4 的推荐结果: [(黄山, 3.5), (张家界, 1.65)] user_5 的推荐结果: [(西湖, 3.0), (故宫, 2.4)]为什么会有 0 分当近邻用户都没有对某个景点产生过行为时算法无法给出有效预测只能返回 0。真实项目中这种情况通常会用热门景点的平均分做填充避免推荐列表为空。4.4 用 Flask 封装推荐接口算法本身跑通后需要封装成 HTTP 接口给 uniapp 调用。这里用 Flask 写一个最简单的服务。文件路径recommend/app.pyfrom flask import Flask, request, jsonify from data import rating_matrix, user_names, spot_names from collaborative_filtering import UserCF app Flask(__name__) model UserCF(rating_matrix, user_names, spot_names, k3) app.route(/api/recommend, methods[POST]) def recommend(): body request.get_json(silentTrue) or {} user_id body.get(user_id, ) top_n int(body.get(top_n, 5)) if user_id not in user_names: return jsonify({code: 1, msg: 用户不存在}), 404 user_idx user_names.index(user_id) result model.recommend(user_idx, top_ntop_n) data [{spot: name, score: score} for name, score in result] return jsonify({code: 0, data: data}) if __name__ __main__: # 本地调试时可开启 debugTrue生产环境务必关闭 app.run(host0.0.0.0, port5000, debugTrue)启动服务cd recommend pip install flask numpy python app.py4.5 uniapp 前端请求推荐接口uniapp 端封装一个简单的请求工具再在推荐页调用接口。文件路径utils/request.jsconst BASE_URL http://localhost:5000; export function request(url, method GET, data {}) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL url, method, data, header: { Content-Type: application/json }, success: (res) { if (res.data.code 0) { resolve(res.data.data); } else { reject(res.data); } }, fail: (err) { reject(err); } }); }); }文件路径pages/recommend/recommend.vuetemplate view classrecommend-page view classtip为你推荐/view view v-foritem in list :keyitem.spot classrecommend-card clickgotoMap(item.spot) text classname{{ item.spot }}/text text classscore兴趣度{{ item.score }}/text /view /view /template script import { request } from /utils/request.js; export default { data() { return { list: [] }; }, onLoad() { this.loadRecommend(); }, methods: { async loadRecommend() { try { const data await request(/api/recommend, POST, { user_id: user_1, top_n: 5 }); this.list data; } catch (err) { console.error(推荐接口调用失败, err); uni.showToast({ title: 推荐加载失败, icon: none }); } }, gotoMap(spotName) { uni.navigateTo({ url: /pages/map/map?spot${encodeURIComponent(spotName)} }); } } }; /script这里有两个常见坑第一微信开发者工具本地调试时默认不允许请求http://localhost需要在开发者工具右上角“详情 - 本地设置”里勾选“不校验合法域名”第二真机调试时localhost指向的是手机本身需要改成电脑在局域网中的 IP 地址。5. ECharts 图形化分析5.1 uniapp 中集成 ECharts 的两种方式uniapp 集成 ECharts 主要有两种方式在 HBuilderX 插件市场搜索echarts或lime-echart插件导入到项目uni_modules目录下。在 CLI 项目中执行npm install echarts然后在小程序 canvas 节点上手动初始化。如果你是 CLI 项目推荐使用 npm 方式。小程序端 ECharts 不能直接使用 DOM需要通过uni.createSelectorQuery获取 canvas 节点再调用echarts.init完成初始化。下面以这种方式演示。5.2 封装一个 ECharts 图表组件先封装一个通用图表组件接收option属性内部负责 canvas 初始化和setOption。文件路径components/echart-panel.vuetemplate view classchart-container canvas v-ifshowCanvas type2d idchart classchart-canvas /canvas /view /template script import * as echarts from echarts; export default { name: EchartPanel, props: { option: { type: Object, required: true } }, data() { return { showCanvas: false, chart: null }; }, watch: { option: { deep: true, handler(val) { this.$nextTick(() this.renderChart(val)); } } }, mounted() { this.showCanvas true; this.$nextTick(() this.initChart(this.option)); }, methods: { initChart(option) { const query uni.createSelectorQuery().in(this); query .select(#chart) .fields({ node: true, size: true }) .exec((res) { if (!res || !res[0] || !res[0].node) { return; } const canvas res[0].node; const ctx canvas.getContext(2d); // 适配小程序 canvas 的像素比避免图表模糊 const dpr uni.getSystemInfoSync().pixelRatio; canvas.width res[0].width * dpr; canvas.height res[0].height * dpr; this.chart echarts.init(canvas); this.chart.setOption(option); }); }, renderChart(option) { if (this.chart) { this.chart.setOption(option); } } }, beforeUnmount() { if (this.chart) { this.chart.dispose(); this.chart null; } } }; /script style scoped .chart-container { width: 100%; height: 300px; } .chart-canvas { width: 100%; height: 300px; } /style这个组件需要注意的地方type2d的 canvas 是小程序新版的同层渲染方式获取的是node节点初始化时一定要设置canvas.width和canvas.height为逻辑尺寸乘以pixelRatio否则图表会明显模糊组件卸载时要调用dispose释放实例否则页面反复进入退出会导致内存泄漏。5.3 热门景点柱状图数据分析页可以拉取后端统计接口拿到热门景点 Top10 数据然后拼装成 ECharts 的 option。template view classstats-page echart-panel :optionbarOption / /view /template script import EchartPanel from /components/echart-panel.vue; import { request } from /utils/request.js; export default { components: { EchartPanel }, data() { return { barOption: {} }; }, onLoad() { this.loadHotSpots(); }, methods: { async loadHotSpots() { try { const data await request(/api/stats/hot-spots, GET); this.barOption { tooltip: { trigger: axis }, grid: { left: 10%, right: 5%, top: 10%, bottom: 10% }, xAxis: { type: category, data: data.map((item) item.name) }, yAxis: { type: value, name: 访问量 }, series: [ { name: 访问量, type: bar, barWidth: 24, itemStyle: { color: #3A7AF5 }, data: data.map((item) item.value) } ] }; } catch (err) { console.error(统计数据加载失败, err); } } } }; /script5.4 游客来源地饼图饼图适合展示比例关系比如游客来源地分布。只需要把 option 的series.type改成pie即可。this.pieOption { tooltip: { trigger: item }, legend: { bottom: 0% }, series: [ { name: 游客来源地, type: pie, radius: [40%, 70%], data: [ { value: 320, name: 华东 }, { value: 240, name: 华南 }, { value: 180, name: 华北 }, { value: 120, name: 西南 }, { value: 90, name: 其他 } ], label: { formatter: {b}: {d}% } } ] };{d}%是 ECharts 内置的百分比占位符饼图每个扇区会自动计算占比不需要手动算。5.5 中国地图可视化说明ECharts 的中国地图在小程序端比柱状图、饼图要麻烦一些因为官方示例中的china.json地图数据需要额外引入或从网络加载。小程序包体积有 2MB 主包限制现在可到 2MB 并通过分包扩大把完整地图数据放进主包会明显增加体积所以更推荐的做法是把地图数据放到服务器运行时通过uni.request加载再调用echarts.registerMap(china, geoJson)注册。const res await uni.request({ url: https://your-cdn.com/geo/china.json }); echarts.registerMap(china, res.data); this.chart.setOption({ series: [{ type: map, map: china, data: [ { name: 浙江, value: 120 }, { name: 广东, value: 200 } ] }] });这里尤其要提醒地图数据的name字段必须和 ECharts 内置地图中的地理名称完全一致比如“浙江”不能写成“浙江省”否则对应区域不会上色。实际开发时可以先打印出地图数据的features[].properties.name做一下比对。6. 腾讯地图 API 集成6.1 申请密钥与域名配置使用腾讯位置服务lbs.qq.com的能力需要先注册开发者账号然后在控制台创建应用添加 Key。创建 Key 时建议开启域名白名单或 Referer 校验避免 Key 被其他网站盗用。在微信小程序里调用腾讯地图 WebService API 时还需要在小程序管理后台把https://apis.map.qq.com配置到“request 合法域名”中否则真机请求会被拦截报错。H5 端则不需要配置域名但需要给 Key 设置对应的 Web 端 Referer 白名单。6.2 获取当前定位uniapp 端用uni.getLocation获取经纬度。微信小程序端在使用前需要在manifest.json中声明位置权限。{ mp-weixin: { permission: { scope.userLocation: { desc: 你的位置信息将用于查找附近景点 } } } }页面中的定位逻辑getLocation() { uni.getLocation({ type: gcj02, isHighAccuracy: true, success: (res) { this.latitude res.latitude; this.longitude res.longitude; }, fail: (err) { console.error(定位失败, err); uni.showToast({ title: 定位失败请检查权限, icon: none }); } }); }注意gcj02是国测局加密坐标小程序地图组件、腾讯地图 API 使用的都是这个坐标体系不要直接使用原始 GPS 坐标否则地图上会出现几百米的偏移。6.3 地图组件展示景点uniapp 的map组件在微信小程序端直接使用微信内置的地图能力底层数据来自腾讯地图。下面是一个最小示例template view classmap-page map idtourismMap classmap :latitudelatitude :longitudelongitude :markersmarkers :scale12 show-location markertaponMarkerTap /map /view /template script export default { data() { return { latitude: 30.2590, longitude: 120.1300, markers: [] }; }, onLoad(options) { if (options.spot) { this.searchSpot(decodeURIComponent(options.spot)); } else { this.getLocation(); } }, methods: { getLocation() { uni.getLocation({ type: gcj02, success: (res) { this.latitude res.latitude; this.longitude res.longitude; } }); }, searchSpot(keyword) { // 调用后端或腾讯地图 POI 搜索返回景点经纬度后设置 markers // 这里为了演示直接赋值 this.markers [{ id: 1, latitude: 30.2590, longitude: 120.1300, title: keyword, iconPath: /static/marker.png, width: 30, height: 30 }]; }, onMarkerTap(e) { console.log(点击了标注点, e.detail.markerId); } } }; /script style scoped .map { width: 100%; height: 100vh; } /style6.4 周边景点搜索与路线规划如果需要“搜索附近的景点”或“从当前位置到景点的路线”可以调用腾讯位置服务 WebService API。示例代码如下searchNearby() { uni.request({ url: https://apis.map.qq.com/ws/place/v1/search, data: { keyword: 景点, location: ${this.latitude},${this.longitude}, radius: 5000, key: 你的腾讯位置服务Key }, success: (res) { if (res.data.status 0) { this.markers res.data.data.map((item, index) ({ id: index, latitude: item.location.lat, longitude: item.location.lng, title: item.title, iconPath: /static/marker.png, width: 30, height: 30 })); } else { console.error(POI搜索失败, res.data.message); } } }); }路线规划可以使用腾讯地图的方向服务接口拿到结果后把路线坐标点解析成polyline传给map组件。比较简单的做法是先调通 POI 搜索再在标注点弹窗里展示“去这里”按钮跳转到uni.openLocation唤起手机自带地图导航。openNavigation() { uni.openLocation({ latitude: this.currentSpot.lat, longitude: this.currentSpot.lng, name: this.currentSpot.title, address: this.currentSpot.address, fail: (err) { console.error(打开地图失败, err); } }); }uni.openLocation在不同端的表现略有差异微信小程序端会打开一个内置地图页面用户可以调用腾讯地图或高德地图完成导航App 端通常唤起系统地图应用。它是最快实现“查看位置、一键导航”能力的方式。7. 常见问题与排查思路下面表格汇总了开发过程中比较容易踩的坑问题现象常见原因解决思路uniapp 运行到微信开发者工具没反应HBuilderX 未配置微信开发者工具路径或开发者工具未开启服务端口微信开发者工具 - 设置 - 安全设置打开“服务端口”检查 HBuilderX 运行配置ECharts 图表白屏canvas 未正确初始化或setOption在实例创建前调用确认createSelectorQuery拿到 node 节点初始化完成后再setOption检查canvas尺寸是否有值图表文字模糊没有按 pixelRatio 缩放 canvas初始化时设置canvas.width 逻辑宽度 * dpr腾讯地图 Key 调用报错小程序请求域名未配置、Key 未开启对应服务或域名白名单不匹配在腾讯位置服务控制台检查 Key 的授权域名小程序后台添加https://apis.map.qq.com到 request 合法域名定位失败用户拒绝授权、未声明scope.userLocation、真机未打开定位检查 manifest.json 权限配置调用uni.authorize引导用户重新授权推荐结果为空或全是 0 分近邻用户对候选景点没有行为或用户行为数据太少用热门景点填充兜底增加行为数据采集考虑换用 ItemCF 或混合推荐真机请求后端接口失败手机无法访问localhost把接口地址改为电脑局域网 IP或部署到公网测试服务器8. 最佳实践与工程建议8.1 密钥与权限管理前端代码里不可避免会出现腾讯位置服务的 Key但 Key 一定要做权限限制。小程序端可以在腾讯位置服务控制台限制该 Key 只允许特定小程序使用H5 端要设置 Referer 白名单。更稳妥的做法是把地图相关请求统一放到后端转发前端只请求自己的后端接口后端再调用腾讯地图 WebService API这样即使前端代码被反编译也不会直接暴露 Key。位置权限属于用户敏感信息必须在工具类中统一封装。小程序审核时官方会检查manifest.json中的隐私说明和代码中的授权逻辑所以desc字段要写清楚“你的位置信息将用于查找附近景点”之类用途说明不能随意写。8.2 算法工程化落地建议演示项目的协同过滤算法是每次请求时实时计算的真实项目中这样写性能并不可靠。建议按下面的思路优化离线计算 结果缓存。用户数量上来以后每天凌晨用定时任务计算一次推荐结果存入 Redis 或 MySQL用户请求时直接读缓存。处理冷启动。新用户没有行为数据时返回全局热门景点新景点没有评分时用平均分或热度分兜底。混合推荐。协同过滤加规则策略比如根据城市、季节过滤掉不合适的景点再结合运营配置的置顶位一起输出。行为权重动态调整。不同行为的权重不应固定写死建议放到配置中心方便运营调整。8.3 ECharts 与地图性能优化小程序包体积非常敏感。ECharts 支持按需引入只引入用到的图表类型和组件可以明显减少打包体积import * as echarts from echarts/core; import { BarChart, PieChart } from echarts/charts; import { GridComponent, TooltipComponent, LegendComponent } from echarts/components; import { CanvasRenderer } from echarts/renderers; echarts.use([BarChart, PieChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer]);地图页的 markers 数量也要控制。地图组件一次渲染几百个 marker 会明显卡顿可以给地图增加视野变化监听事件只显示当前可视区域内的景点也可以对多个近距离景点做聚合展示点击后再展开。8.4 上线检查清单项目准备发布到微信小程序前建议逐项确认检查项说明合法域名request 合法域名已配置包含后端接口和腾讯地图 API隐私协议用户协议、隐私政策已在后台填写弹窗授权逻辑已实现位置权限说明manifest.json中scope.userLocation的 desc 已填写接口鉴权推荐接口和统计接口有简单的鉴权机制避免任意用户刷接口包体积主包小于 2MB超大资源放入分包或用 CDN数据脱敏用户行为数据上报时不传明文手机号等敏感字段后端环境Flask 生产环境关闭 debug使用 gunicorn 等 WSGI 服务器如果后续要接入小程序支付需要先确认主体资质个人小程序无法开通支付能力只能用体验版做流程演示。支付属于资金敏感链路必须使用官方 SDK 并在沙箱/测试环境充分验证后再上线。9. 总结与学习路线做完这个项目后我最大的感受是uniapp 本身并不难难的是跨端差异和第三方能力的边界。ECharts 在小程序端需要 canvas 节点适配腾讯地图在小程序端可以直接用内置 map 组件而到了 H5 端又可能走不同的 API这些差异只有实际跑一遍才会真正记住。如果你打算继续深入下一步可以按这样的顺序进阶先把协同过滤算法从 UserCF 扩展到 ItemCF对比两类算法在同一份数据上的推荐差异然后把推荐结果缓存到 Redis模拟真实项目的离线计算链路再给小程序增加用户登录和收藏功能让行为数据真正闭环最后研究 uniapp 的打包上架流程把小程序发布到体验版让真实用户测试。推荐算法的天花板不在算法本身而在数据质量和工程落地。先把用户行为数据采好、把推荐结果解释清楚再用更复杂的模型提升效果这才是比较稳妥的成长路径。希望这篇实战笔记能给你带来一些可复用的思路。
返回列表