☰
UniApp旅游小程序推荐系统实战:标签画像与协同过滤落地
2026/10/7 5:06:46 网站建设 项目流程

去年接了个旅游类的小程序需求,产品经理的诉求特别直白:"用户打开小程序,能看到几个合口味的景点,而不是一屏全是网红打卡地"。听起来简单,真正落地的时候才发现,"推荐"这两个字后面藏着标签体系、用户画像、行为采集、冷启动一长串问题。最终项目选择了UniApp这套跨端框架来做微信小程序端,推荐引擎没有上机器学习,而是用了一套轻量的标签加权加协同过滤方案,实测效果在中小体量数据下完全够用。

这篇文章把整个项目的技术链路拆开讲一遍,从技术选型、推荐算法落地、UniApp开发联调,到发布上线前的配置坑,尽量按实操顺序来。适合正在做小程序开发、或者准备用UniApp接旅游类推荐需求的工程师参考,有数据规模不大、想快速落地推荐功能的朋友也可以直接抄作业。

1. 为什么这个项目最终选了UniApp:从技术选型到架构拆分

1.1 需求本质:表面是推荐系统,底层是内容分发

先把需求拆清楚。所谓旅游景点推荐系统,本质上是一个"内容分发"问题:数据库中可能有几千上万个景点POI(兴趣点),每个POI有坐标、门票、评分、标签、图片、简介。用户打开小程序时,不可能把所有POI一次性倒给用户,手机屏幕就那么大,用户耐心就那么几秒,必须做到"千人千面",把最可能符合当前用户偏好的几个景点推到首页。

这里有个容易犯的错误:一提到推荐,就想着上协同过滤、上深度学习、上向量召回。对一个小程序项目来说,数据量可能只有几千个景点、几千个日活用户,跑这么重的框架纯属给自己加班。推荐系统的目标不是炫技,而是让用户在有限的信息流里快速找到想去的地方。

围绕这个目标,我把系统拆成五个核心模块:

  • 景点管理:后台录入景点的名称、坐标、标签、图片、评分、开放时间等基础数据
  • 用户模块:微信登录、用户画像、浏览记录、收藏、足迹
  • 推荐引擎:基于标签和行为的召回排序服务,输出个性化景点列表
  • 小程序端:首页信息流、景点详情、地图附近、个人中心
  • 管理与运营:热门景点人工干预、兜底策略配置

1.2 UniApp对比原生开发的取舍点

这个项目最初也纠结过要不要用微信原生小程序开发,后来团队评估完还是选了UniApp。考虑因素如下表:

维度UniApp微信原生小程序
多端复用一套Vue代码可编译到微信小程序、App、H5仅微信端,要做App得另起炉灶
开发效率Vue单文件组件,生态丰富,上手快原生WXML/WXSS,写法偏自定义
性能经HBuilderX编译后接近原生,略有一层转换开销性能最优,组件和API最底层
周边生态插件市场有大量现成组件微信官方组件和API最全
技能复用Vue技术栈可迁移到Web/App原生技能只能用在微信生态

这个项目有一个隐含需求,后续可能要做App端或者H5端,如果一开始用原生小程序写,将来迁移等于重写一遍。UniApp虽然性能上有一点折损,但对一个以信息展示和推荐分发为核心的小程序来说,性能瓶颈根本轮不到框架层,真正的瓶颈在图片体积和接口设计上。

1.3 数据模型设计:推荐系统的地基

数据模型在动手写代码之前就得定好。推荐系统最忌讳"先开发页面,后补数据",因为画像、行为、标签这些数据如果不在源头设计好,后面全得返工。

我用几张核心表来支撑推荐逻辑:

  • 景点表(poi):id、名称、城市、经度、纬度、评分、热度、封面图、标签IDs
  • 标签表(tag):id、标签名(如"亲子""自然""人文""美食")
  • 景点标签关联表(poi_tag):poi_id、tag_id
  • 用户行为表(user_behavior):id、用户ID、景点ID、行为类型(浏览/收藏/搜索)、发生时间
  • 用户画像表(user_profile):id、用户ID、标签权重JSON、偏好城市、活跃度

用户画像表里的标签权重JSON是推荐引擎的核心输入,格式大概是{"自然": 0.8, "亲子": 0.6, "人文": 0.3}。这个权重不是拍脑袋定的,是用户行为通过积分规则累积出来的,后面会细说。

1.4 技术栈全景

最终的技术栈如下:

  • 前端框架:UniApp(Vue 3语法),编译目标为微信小程序
  • 后端服务:Spring Boot + MyBatis Plus,部署在云服务器上
  • 数据库:MySQL 8.0,存放景点、标签、用户、行为数据
  • 缓存:Redis,用来缓存推荐结果和热点数据
  • 推荐计算:Java后端定时任务 + 轻量算法,输入用户ID,输出有序景点ID列表

小程序端不需要承担推荐计算任务,只负责展示结果。这个分离很重要,小程序端跑复杂计算既耗性能又费流量,而且推荐逻辑更新时还必须发版,放在服务端随时可调。

2. 推荐引擎不堆算法:标签画像、召回排序与冷启动的落地实现

2.1 景点标签和用户画像:推荐的地基

推荐系统的地基不是算法,而是标签。一个景点如果没有任何标签,推荐引擎就算把数学玩出花来也算不出它适合谁。

我先给景点规划了基础标签体系,按旅游决策场景分成几个维度:

  • 主题类型:自然风光、人文古迹、主题乐园、城市漫步、宗教寺庙、科普研学
  • 人群偏好:亲子、情侣、老人、朋友聚会、独自旅行
  • 场景属性:网红打卡、小众秘境、摄影胜地、夜游、徒步登山、水上项目

每个景点建议打3到5个标签,宁缺毋滥。比如西湖可以打"自然风光""城市漫步""网红打卡""情侣",但不要打"主题乐园"这种明显不符合的标签。标签不准,后面画像再准也白搭。

用户画像的构建思路是"行为映射到标签":用户浏览了一个景点,就把这个景点的标签权重累加到用户的标签权重上,再乘时间衰减因子。

2.2 召回与排序:一条公式把推荐跑起来

推荐的完整流程分召回和排序两步。

召回阶段,根据用户的画像标签,从景点库里捞出一批候选景点。简单做法是:遍历所有景点,计算每个景点标签集合与用户标签权重的匹配度,取TopN作为候选集。这里我用余弦相似度计算用户画像和景点标签之间的匹配分:

matchScore = (用户标签权重向量 · 景点标签向量) / (用户画像权重模长 × 景点标签模长)

这个值在0到1之间,越接近1说明这个景点越符合用户的兴趣偏好。

排序阶段,在召回集的基础上加距离、热度、评分做加权。最终评分公式是:

finalScore = 0.4 × 标签匹配度 + 0.3 × 距离因子 + 0.2 × 热度分 + 0.1 × 评分分

其中距离因子按线性衰减:1 - min(实际距离/搜索半径, 1),也就是说距离越近得分越高。热度分用景点的访问量归一化得到,评分分用景点的用户评分除以5得到。权重可以根据运营需要随时调整,比如旅游旺季可以把距离因子调高,引导用户去附近景区,而非大老远跑到别的城市。

这部分在Java里就是一次简单的遍历计算,几千个景点性能完全没问题。核心代码如下:

public List<Poi> recommend(Integer userId, Double lat, Double lng, int topN) { // 1. 读取用户画像标签权重 UserProfile profile = userProfileMapper.selectByUserId(userId); // 2. 召回:遍历景点,计算标签匹配度 List<Poi> allPois = poiMapper.selectAll(); List<ScoredPoi> scoredList = new ArrayList<>(); for (Poi poi : allPois) { double tagScore = calcTagMatch(profile.getTagWeights(), poi.getTags()); double distance = DistanceUtil.distance(lat, lng, poi.getLat(), poi.getLng()); double distanceFactor = Math.max(0, 1 - distance / searchRadius); double hotScore = poi.getHotRank() / maxHotRank; double ratingScore = poi.getRating() / 5.0; double finalScore = 0.4 * tagScore + 0.3 * distanceFactor + 0.2 * hotScore + 0.1 * ratingScore; scoredList.add(new ScoredPoi(poi, finalScore)); } // 3. 排序取TopN scoredList.sort((a, b) -> Double.compare(b.getScore(), a.getScore())); return scoredList.subList(0, Math.min(topN, scoredList.size())) .stream().map(ScoredPoi::getPoi).collect(Collectors.toList()); }

这里还有一个协同过滤的轻量落地。当用户的行为数据积累到一定量后,可以基于"看了A景点的人也看了B景点"做关联推荐。实现不复杂:在用户行为表里统计共现次数,建立景点间的共现矩阵,存入Redis,当推荐结果里标签匹配度都太低时,用协同过滤结果做补充。

2.3 冷启动方案:新用户不看空页面

冷启动是推荐系统绕不开的问题。新用户没有任何行为数据,画像为空,如果直接跑推荐公式,所有景点的标签匹配度都是0,最终只能按距离、热度、评分排序,展示效果跟"随便看看"没有区别,这就失去了推荐的意义。

我的冷启动方案分三层:

  • 第一层:根据用户当前定位,推荐"附近热门景点Top10",用城市优先兜底
  • 第二层:如果用户授权了手机号,尝试根据微信登录返回的性别年龄信息(如果有)做粗略画像匹配
  • 第三层:在首页推荐流里混入几类热门标签的代表性景点,每个标签出1到2个,既保证多样性,也给用户提供"点开即训练画像"的素材

冷启动阶段最忌讳的是推荐结果过于单一。比如用户在大理,就给他推10个洱海周边景点,结果用户觉得全是海景房。我的做法是强制"标签多样":候选集按标签分桶,每个桶取Top2,然后拼接成最终的推荐流。

2.4 为什么不在小程序端直接算推荐

这个项目从一开始就定了一个原则:小程序端坚决不做推荐计算。原因有三。

第一,数据安全问题。推荐逻辑放在前端,等于把整个推荐策略暴露给用户,运营和商业价值全没了。考察一下竞品,没有哪个成熟产品会在客户端直接跑核心算法。

第二,性能问题。小程序端JavaScript引擎处理几千条数据的遍历计算虽然不至于卡死,但在低端安卓机上,用户滑个列表还要等本地算完才有数据,体验很差。

第三,更新问题。推荐策略必须能快速迭代。周末景区人流量异常,运营想临时调权重,如果逻辑在前端,就得提审、发版,等两三天审核,黄花菜都凉了。放在服务端,Redis配置一改,秒级生效。

小程序端只做一件事:调用后端接口拿推荐结果,再渲染页面。这样职责清晰,也方便后续推荐逻辑升级。

3. 从HBuilderX创建工程到微信开发者工具跑通:开发链路全记录

3.1 HBuilderX创建项目与工程结构

UniApp开发我用的HBuilderX,它是DCloud官方的IDE,对UniApp的支持最无缝,创建、编译、调试、打包一站式解决。用命令行的方式也可以,但HBuilderX对新手更友好,不用自己配一堆环境变量。

创建项目的流程:打开HBuilderX → 文件 → 新建 → 项目 → 选择"uni-app"模板 → 项目名称填travel-recommend-miniapp,框架选Vue 3。HBuilderX会自动生成一套标准的UniApp目录结构。

创建完的目录如下,每个目录职责要明确:

travel-recommend-miniapp/ ├── pages/ # 页面目录,所有页面都放这 │ ├── index/ # 首页推荐Feed │ ├── explore/ # 附近景点/地图页 │ ├── detail/ # 景点详情页 │ └── mine/ # 个人中心 ├── components/ # 可复用组件(景点卡片、标签等) ├── api/ # 接口请求封装 ├── static/ # 静态资源 ├── utils/ # 工具函数(距离计算、格式化等) ├── App.vue # 应用入口,生命周期,全局样式 ├── main.js # Vue实例入口 ├── manifest.json # 应用配置,包含小程序AppID等 ├── pages.json # 页面路由和导航栏配置 └── uni.scss # 全局样式变量

pages.json相当于小程序原生里的app.json,页面路由、TabBar、导航栏样式全在这里配置。这个文件我建议一开始就完整配置好,别等页面多了再补,后面路由跳转全依赖它。

3.2 manifest.json和AppID配置:最容易被忽略的第一步

这是整个开发流程里最容易踩坑的地方。manifest.json里有个"微信小程序配置"区块,里面的appid一定要填真实的小程序AppID,否则微信开发者工具编译后照样会报错或功能受限。

获取AppID的流程:微信公众平台注册小程序账号,登录后在"开发 > 开发管理 > 开发设置"里可以找到AppID。这里分两种:测试号AppID和正式AppID。开发阶段可以用测试号,但调试登录、获取手机号等能力时测试号有限制,建议尽早用正式号。

配置路径:manifest.json → 微信小程序配置 → 填入AppID。填完之后,还要在HBuilderX里设置"微信开发者工具运行路径"。在HBuilderX的"运行 > 运行到小程序模拟器 > 微信开发者工具"里,第一次会要求指定微信开发者工具的安装路径。这里注意,微信开发者工具需要开启"服务端口"才能接收HBuilderX的推送编译。

微信开发者工具设置路径:右上角设置 → 安全设置 → 服务端口 → 打开。不开这个端口,HBuilderX编译完根本推不进去。

3.3 微信开发者工具联调:从编译到真机预览

配置完成后,HBuilderX工具栏点"运行 > 运行到小程序模拟器 > 微信开发者工具",编译完会自动打开微信开发者工具加载项目。这里的联调流程我建议养成固定的习惯:

  • 先看微信开发者工具的Console面板,有红色报错优先处理
  • 网络请求在Network面板看,开发阶段记得勾选"不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书"
  • 真机预览用微信扫一扫,手机上装的是体验版,注意开发者工具上"预览"按钮生成的二维码有时效

开发阶段总会遇到一个窒息场景:真机上样式和模拟器里完全不一样。模拟器用的是Chromium内核渲染,真机是XWeb内核,部分CSS属性支持度有差异。遇到这种问题我的做法是尽量用Flex布局,少用绝对定位,圆角和阴影适当保守一点。这个项目首页Feed卡片曾经在模拟器里好好的,真机上出现底部留白问题,排查发现是padding-bottom和env(safe-area-inset-bottom)冲突导致的,后面统一用安全区适配才解决。

3.4 菜单路由、TabBar与页面间通信

TabBar配置在pages.json里,我配了三个:首页推荐、附近探索、我的。图标用的是iconfont字体图标,放在static目录。这里有个小技巧:TabBar的图标尺寸建议遵循微信官方规范,81px × 81px,图标过大或过小都会影响真机显示。

页面跳转要注意,UniApp里uni.navigateTo和uni.switchTab是有区别的。navigateTo用于页面栈内的普通页面跳转,switchTab用于跳转到TabBar页面,两者不能混用。从景点详情页返回首页时,如果用了navigateBack,页面栈里可能没有上一页,会直接卡死。我的做法是详情页入口统一用navigateTo,详情页里放一个"返回首页"按钮,用uni.reLaunch直接重置页面栈到首页。

页面间传参我用三元运算符式的URL传参,简单直接。比如详情页跳转:

uni.navigateTo({ url: `/pages/detail/detail?id=${poiId}` });

接收参数时用onLoad(options)里的options.id。复杂对象传参用eventChannel或者全局状态管理,但项目里我尽量少用全局Store,因为小程序页面刷新后全局数据容易丢,还是要以URL参数和本地缓存为主。

4. 景点Feed、地图定位与"附近推荐":三个核心页面的实现细节

4.1 首页推荐Feed:组件拆分与骨架屏

首页是整个小程序的门面,推荐流长什么样直接决定用户愿不愿意留。把首页拆成三个区块:顶部城市定位 + 标签偏好快捷选择、推荐景点卡片Feed、底部加载更多。

推荐卡片我抽成了一个组件PoiCard,在components目录下。这个组件接收景点对象,渲染封面图、名称、标签、评分、距离信息。组件化有两个好处:详情页关联推荐、探索页列表都要用同一套卡片样式,改样式时只改一处。

卡片布局用的是经典的上下结构:上面是大图,下面叠加文字信息。封面图这块有个性能细节:图片尺寸一定要压缩。微信小程序单包限制2MB,图片资源过大不只影响体积,还会在列表滚动时产生白屏和卡顿。我的做法是后端接口直接返回裁剪后的图片URL,统一缩放到750px宽度,使用WebP格式,实测体积能小60%以上。

骨架屏是提升首屏体验的关键。推荐接口返回前,页面不能白着,用骨架屏占位,给用户"马上就好"的暗示。UniApp里实现骨架屏很简单,在页面loading状态渲染几个灰色块模拟卡片布局。重点:骨架屏样式要跟真实卡片保持一致,否则用户会感觉页面跳了一下。

接口请求封装我放在api目录下,代码如下:

// api/index.js const BASE_URL = 'https://api.example.com'; export function getRecommendList(data) { return new Promise((resolve, reject) => { uni.request({ url: `${BASE_URL}/api/recommend`, method: 'GET', data, success: (res) => resolve(res.data), fail: (err) => reject(err) }); }); }

这里注意,不要每次请求都写一遍uni.request,封装成Promise后,页面里用async/await调用,代码会清爽很多。Loading状态我统一用页面级变量控制,不用uni.showLoading,因为showLoading在快速频繁请求时会出现闪烁,体验很糟。

4.2 地图组件与景点标记:探索页的核心

探索页的地图用的是UniApp内置的map组件,它底层映射微信小程序的map组件。实现逻辑不复杂:拿到用户当前定位坐标,然后查询附近的景点POI,在map上渲染marker标记点。

map组件的核心配置:

<map :latitude="lat" :longitude="lng" :markers="markers" :scale="12" show-location @markertap="onMarkerTap"> </map>

marker数据要按微信的要求格式组织:id、latitude、longitude、iconPath、width、height。iconPath可以是自定义的景点类别图标,比如自然景点用一个绿色树图标,人文景点用一个红色建筑图标,这样地图一眼看过去就很有信息层。

@markertap事件返回选中的marker的id,根据id去查询景点详情再跳转。这块有个容易忽略的点:map组件在页面中占位很大,如果地图页和列表页共用同一个页面,建议用tab切换而不是两个页面互跳,这样地图状态和滚动位置都能保持。

4.3 定位授权与距离计算:家政服务式的本地化推荐

距离计算是旅游推荐绕不开的能力。获取定位有两条路:

  • uni.getLocation:获取经纬度
  • 微信原生wx.chooseLocation:让用户在地图上手动选点

我的场景是首页自动定位城市、探索页获取附近景点,所以用uni.getLocation就够了。

uni.getLocation({ type: 'gcj02', isHighAccuracy: true, success: (res) => { this.lat = res.latitude; this.lng = res.longitude; // 调用推荐接口,传入坐标 this.fetchRecommend(); }, fail: () => { // 用户拒绝授权,用默认城市兜底 uni.showToast({ title: '定位失败,显示默认推荐', icon: 'none' }); } });

坐标类型用gcj02,这是国测局坐标标准,微信小程序内map组件和接口返回的景点坐标都用这个,不要误用wgs84,否则地图上标记位置会偏几百米。

拿到坐标之后,距离计算是后端做的,前端只负责展示。两点距离用Haversine公式算,这个公式就几行代码,Java里用Math库就能写:

public static double distance(double lat1, double lng1, double lat2, double lng2) { double radLat1 = Math.toRadians(lat1); double radLat2 = Math.toRadians(lat2); double a = radLat1 - radLat2; double b = Math.toRadians(lng1) - Math.toRadians(lng2); double s = 2 * Math.asin(Math.sqrt( Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2) )); return s * 6371.0; // 地球半径6371公里 }

注意不要在JS前端算距离。一是后端算可以按距离参与推荐排序,二是前端JS的浮点精度偶尔会出幺蛾子,显示距离多出几十米会被用户吐槽。

4.4 图片资源优化与分包策略

这个项目踩过一个大坑:开发阶段图片全放在static目录里,用了很多体积很大的JPG。结果微信开发者工具编译时报"主包体积超过2MB限制"。后来我把策略改成:

  • 所有景点图片全部走CDN,按需加载,不进小程序包
  • 只有TabBar图标、默认占位图等必须的本资源才放在static
  • 图片URL由后端接口下发给前端,前端直接用<image>标签的:src属性渲染

对于实在需要打进包里的资源,用image组件的懒加载属性lazy-load,滚动列表时图片进入视口才开始加载,性能提升明显。

如果页面数量多,还可以用微信小程序的分包加载能力,把"景点详情页"等低频页面拆到分包里,主包只放TabBar三个主页面,这样主包体积能控制在2MB以内。UniApp里在pages.json中配置"subPackages"字段即可。

5. 登录鉴权和行为埋点:让推荐系统越用越准的数据闭环

5.1 uni.login静默登录与用户身份绑定

推荐系统要形成数据闭环,前提是知道"谁在操作"。微信小程序登录用的是uni.login,它的核心是拿到一个code,然后后端拿着code去微信服务器换openid和session_key。

uni.login({ provider: 'weixin', success: async (loginRes) => { const res = await api.login({ code: loginRes.code }); uni.setStorageSync('token', res.data.token); } });

后端的处理逻辑是:接收前端传上来的code,调用微信的https://api.weixin.qq.com/sns/jscode2session接口,拿到openid。openid是用户的唯一标识,首次登录则创建用户记录,已存在则直接返回token。

整个登录过程对用户是无感的,不用弹窗,不用授权,所以叫"静默登录"。用户第一次打开小程序时,如果用户没有任何明确动作,只要触发了uni.login,后端就已经把这个用户识别出来了。这里的关键点:用户画像和推荐结果都可以先产生,等用户授权手机号后,再把手机号绑定到已有用户记录上,而不是等手机号授权了才建立用户账户。

5.2 手机号授权的正确打开方式

获取手机号这个功能是微信小程序商业化的常见需求,比如做会员系统、优惠券触达。它需要用到button组件的open-type="getPhoneNumber",然后前端拿到code传给后端换手机号。

代码示例:

<button open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber">授权手机号</button>
onGetPhoneNumber(e) { if (e.detail.errMsg === 'getPhoneNumber:ok') { // 将code发送给后端 api.bindPhone({ code: e.detail.code }) .then(res => { uni.showToast({ title: '绑定成功', icon: 'success' }); }); } else { // 用户拒绝授权,不要一直弹窗 uni.showToast({ title: '未授权手机号', icon: 'none' }); } }

后端拿这个code调用微信接口换手机号,注意必须传openid,因为手机号换取的接口要求用户必须已经登录,否则返回错误码。

这是产品设计上的一个坑:不要一进小程序就强制用户授权手机号。微信对这种强制授权的审核是零容忍的,而且用户会有强烈的被冒犯感。我的做法是:用户第一次点击"领取优惠券"或"查看完整行程"这类业务需要时,才弹出手机号授权,并且允许用户拒绝。拒绝后仍然可以正常浏览景点,只是部分需要手机号的功能暂时不可用。

5.3 行为埋点:浏览、收藏、搜索的数据采集

有了用户身份,下一步是采集行为。推荐系统的AI含量全靠行为数据的质量,埋点设计直接影响模型效果。

我埋了四类行为,分别对应不同场景:

行为类型触发时机权重
浏览进入景点详情页1
收藏点击收藏按钮3
搜索搜索关键词2
停留时长页面停留超过10秒1.5

埋点上报用uni.request异步发送,不阻塞用户操作。为了防丢,我在本地的storage里累积了上报事件,然后定时批量上传。这里注意:埋点接口和业务接口最好分开,不要互相阻塞,否则埋点报错会影响正常页面加载。

采集到的行为数据进入user_behavior表后,后端定时任务会定期更新用户画像。有个容易被忽略的细节:行为数据只有用户处于登录状态才有意义。如果在用户未登录时就产生行为,这个行为数据要么不收集,要么等用户登录后再挂载到用户ID上。我是用本地缓存的临时ID来标记未登录用户,登录后把临时ID关联到正式用户ID,避免丢数据。

5.4 画像更新与推荐结果刷新机制

用户画像更新不能每次行为都实时全量计算,那样数据库压力很大。我设计了三个更新粒度:

  • 实时更新:用户点击收藏按钮时,同步更新画像中的标签权重,让用户立刻感知到推荐变化
  • 定时更新:每天凌晨跑一次批处理,根据当天全部行为重新计算画像
  • 手动触发:用户下拉刷新推荐列表时,强制后端做一次增量计算

推荐结果的缓存策略是:推荐结果按用户ID缓存10分钟。用户在10分钟内重复进入首页,直接返回缓存,不用每次重新计算。但如果用户产生了新的行为,则主动清除该用户的推荐缓存,让下一次请求重新计算。这样既能保证实时性,又不至于频繁计算。

最关键的还是"反馈闭环":用户往下滑了很久,点进去看的永远是同一种类型的景点,那说明画像更新可能太滞后了。我会运营同学配合做一个小功能:在推荐流里插入"不喜欢"按钮,用户点掉某个景点后,这个景点的标签权重在当前会话内降低50%,系统会减少同类景点的推荐。这个功能虽然偶尔被吐槽"点不干净",但在画像冷启动阶段非常有用。

6. 发布上线前的配置清单与真机踩坑记录

6.1 request合法域名:上线卡脖子的第一道坎

开发阶段可以在微信开发者工具里勾选"不校验合法域名",上线后这个选项就失效了。微信小程序要求所有uni.request请求的域名必须在微信公众平台后台配置为request合法域名,并且必须是HTTPS。

配置路径:微信公众平台 → 开发管理 → 开发设置 → 服务器域名 → request合法域名。

坑点集中在三个方面。

第一,域名必须备案,而且不能是IP地址。很多团队开发时用http://192.168.1.100:8080,开发阶段没问题,上线前必须换成已备案的HTTPS域名。

第二,域名配置生效有延迟,修改后建议等5到10分钟再测试,不要一改完就狂请求然后怀疑人生。

第三,正式环境不要用uni.request直接访问IP地址或内网域名,微信审核时会直接驳回。遇到过同行为了快速上线,把后端接口放在阿里云函数计算上,结果域名没备案,小程序审核被连续驳回两次,最后只能临时买了个备案域名才解决。

我的建议是:项目建好后第一周就去申请备案域名并配置HTTPS证书,开发阶段就开始用正式域名做联调,不要等到上线前再来折腾。

6.2 主包2MB限制:图片和代码怎么瘦身

微信小程序主包限制2MB,这个数字卡死了无数项目。UniApp项目编译后,JavaScript、WXML、WXSS、静态资源都会计入体积。我的瘦身步骤:

  • 第一步,把图片全部移到CDN,远程图片不计入主包体积
  • 第二步,不用的组件和库坚决删掉。比如开发阶段引了moment.js做时间处理,一个库就300KB,后来换成自己写的几行格式化函数,省下来的空间非常可观
  • 第三步,低频页面拆到分包,详情页、搜索页、我的足迹都放subPackages里
  • 第四步,压缩静态资源,字体图标能用SVG就不要用多张PNG

编译后如果提示source size exceeds max limit,看HBuilderX的编译日志可以知道具体是哪个文件占了大头。我有一个临时"大招":如果实在超了,可以把整个页面用大图懒加载,把一些组件改成按需注入,但这个只能应急,根治还是在资源管理。

6.3 顶部导航栏和胶囊按钮的高度适配

微信小程序的导航栏很特殊,不同机型上胶囊按钮(右上角的"..."和"○")高度不同,刘海屏和非刘海屏的状态栏高度也不同。如果页面里有自定义导航栏(navigationStyle: custom),就得自己适配高度。

获取正确高度的方法:

const systemInfo = uni.getSystemInfoSync(); const statusBarHeight = systemInfo.statusBarHeight; // 状态栏高度 const menuButton = uni.getMenuButtonBoundingClientRect(); // 胶囊按钮位置信息 const navBarHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height;

这个计算结果就是自定义导航栏的总高度,页面顶部留出statusBarHeight + navBarHeight的空间,导航栏内容(标题、返回按钮)就不会被胶囊按钮挡住。

坑点提醒:uni.getMenuButtonBoundingClientRect()在部分安卓机型上返回值可能不准确,建议在onReady之后调用,并且处理异常情况设置一个默认值(比如45px)。另外,iPhone 14 Pro等带灵动岛的机型状态栏高度更高,不要写死,必须动态计算。

6.4 体验版发布、审核注意事项与灰度策略

开发完进入发布流程时,有几个容易忽略的细节。

体验版不是正式版。微信开发者工具点"上传"按钮,填好版本号和备注,然后到微信公众平台"版本管理"里把该版本设为体验版。体验版二维码发给测试人员,测试人员必须成为小程序的体验成员才能打开。这里注意,体验版和正式版的域名校验都是强制生效的,不能因为"正在测试"就跳过HTTPS。

审核环节有几个硬性要求需要提前自查:

  • 小程序首页必须有实质内容,不能是一张海报或一个登录框
  • 涉及旅游信息展示,最好在页面底部放上免责声明,比如"景点信息仅供参考,请以景区实际开放情况为准"
  • 用户隐私协议必须明确说明收集了位置信息和行为数据,微信审核对隐私协议越来越严格
  • 操作按钮不能诱导分享,比如"分享给好友解锁更多景点"这类逻辑会被判违规

灰度策略上,我习惯在上线初期用"白名单灰度":后端配置一个灰度开关,只让内部测试账号走新版推荐算法,其他用户走旧版本兜底。等观察一段时间数据表现稳定,再逐步放量到100%。这样做最大的好处是,推荐逻辑有问题时不会直接暴露到所有用户面前,回滚也快。

最后说一句个人体会

做这个项目最大的感悟是:推荐系统在小程序场景下,算法权重远不如数据干净度重要。标签打准、行为埋点不漏、用户身份尽早统一,这三件事做好了,哪怕只用简单的公式排序,效果也能吊打那些数据一团糟却硬上深度学习的项目。如果你正准备做类似的需求,我的建议是先把数据模型和埋点设计清楚,再谈推荐策略,顺序反了后面会一直在填坑。另外,上线前一定要用自己的真机完整走一遍从启动到授权的流程,模拟器里跑得通不代表真机上不会出问题。这套方案在中小体量的旅游推荐场景下我已经跑过一遍,你可以放心照着这个思路去搭。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询