
简介微信小程序开发常受困于服务器搭建、域名备案等基础设施问题云开发将数据库、存储和云函数整合为开箱即用的后端方案让开发者专注于业务逻辑。其原理是通过云函数在服务端读写云数据库小程序端仅调用接口既保证数据安全又简化权限管理。这种架构尤其适合内容展示型应用如菜谱小程序这类读多写少的场景可快速实现菜谱列表、详情、收藏和搜索功能。本文基于一份云开发菜谱小程序源码讲解工程结构、云环境配置、数据库集合设计、云函数部署全流程并总结主包超限、真机图片失效、冷启动超时等高频避坑经验帮助个人开发者从零跑通上线。1. 拿到的这份菜谱小程序源码先看清楚它省掉了哪些事把这份菜谱微信小程序源码从 .rar 压缩包里解压出来你大概率会看到两个顶层目录miniprogram和cloudfunctions。前者跑在小程序端后者跑在云开发的 Node.js 运行时里。整个项目不依赖自购服务器不需要域名备案和 HTTPS 证书配置前后端共用同一套账号体系——这是当下个人做微信小程序最省事的路线之一。这类源码解决的核心问题是「一个人把一个完整产品从 0 推到上线」。云开发把数据库、文件存储、云函数三项最重的后端能力打包成开箱即用的服务你只需要关注页面和业务逻辑。它的目标用户很明确没有后端团队的个人开发者、想快速验证菜谱类产品原型的内容创业者以及手里已有菜谱资源想做成小程序分发的美食博主。2. 云开发和源码目录为什么菜谱类小程序最吃这套架构2.1 为什么我不建议给菜谱项目自建服务器菜谱类小程序是典型的读多写少应用用户高频浏览菜品列表、详情、步骤但写入行为只有收藏、浏览记录、评论等低频操作。这种负载特征决定了它对后端的需求不是「强计算」而是「够用的数据存取 快速上线」。如果我按传统方案来做需要买一台云主机或轻量服务器、注册域名、做 ICP 备案、配 Nginx、装 MySQL 或 MongoDB、再写一套鉴权接口。这里面每一项都不是写业务代码而是铺基础设施。对一个以内容展示为主的菜谱项目来说这套流程要消耗的时间往往比写小程序页面本身还多。云开发把这几件事合并成一次开通操作你在微信开发者工具里点一下创建环境数据库、存储、云函数运行时就都有了。菜谱项目尤其适合云开发还有一个原因菜谱数据天然以「图片 结构化文本」为主。云存储专门管理图片文件云数据库存结构化字段云函数处理分页和聚合查询。三者的组合恰好覆盖一道菜从封面、食材、步骤到收藏统计的全部数据形态。此外云开发按调用量和存储量计费冷启动阶段几乎花不了多少钱。2.2 源码包里的两个世界miniprogram 与 cloudfunctions常见的云开发源码包会把工程按功能拆分归档目录结构大致如下菜谱小程序源码/ ├── miniprogram/ │ ├── app.js │ ├── app.json │ ├── pages/ │ │ ├── index/ # 首页轮播、分类入口、推荐列表 │ │ ├── category/ # 分类页按菜系/场景筛选 │ │ ├── detail/ # 详情页食材、步骤、收藏按钮 │ │ ├── search/ # 搜索页按菜名/标签搜索 │ │ ├── favorite/ # 收藏页我的收藏列表 │ │ └── profile/ # 个人中心登录态、绑定手机号 │ ├── components/ │ │ ├── recipe-card/ # 菜谱卡片组件 │ │ └── search-bar/ # 搜索栏组件 │ └── utils/ │ └── request.js # 封装云函数调用 ├── cloudfunctions/ │ ├── getRecipes/ # 菜谱列表分页、分类过滤 │ ├── getRecipeDetail/ # 菜谱详情浏览量1 │ ├── collectRecipe/ # 收藏/取消收藏 │ └── login/ # 登录态处理 └── project.config.json这个结构的核心设计是「端」与「云」分离。miniprogram目录里的代码最终会被微信开发者工具编译成小程序包里面不应该出现数据库连接串、密钥等敏感信息cloudfunctions目录下每个子目录都是一个独立的云函数微信开发者工具分别打包上传到云端以 Node.js 函数形式运行。对比一下就能看出选型逻辑菜谱数据表、图片文件都放在云开发侧小程序端只关心「调用哪个云函数、拿到什么结构的数据」。如果以后想把数据迁移到自己的服务器只需要把云函数内部替换成 HTTP 请求页面代码几乎不用动。2.3 一次完整的云开发数据流整个小程序的请求链路可以浓缩成一个循环页面触发事件 → 小程序端调用云函数 → 云函数读写云数据库/云存储 → 返回数据 →setData渲染。// miniprogram/utils/request.js 中常见的云函数调用封装 const callFunction (name, data {}) { return new Promise((resolve, reject) { wx.cloud.callFunction({ name: name, // 云函数名称需与 cloudfunctions 下的目录名一致 data: data, // 业务参数例如 { categoryId, page, pageSize } success: res resolve(res.result), fail: err reject(err) }); }); }; module.exports { callFunction };这里有个容易误解的点wx.cloud.callFunction返回的res.result才是云函数的业务返回而res.result外层还包着一层云开发框架的固定结构。新手经常直接拿整个res去渲染导致页面拿不到数据。只要在success回调里先取.result后面所有页面的代码才能保持统一。这种「页端不直连数据库」的约束很关键。云数据库的客户端 API 虽然可以在小程序里直接用但会把集合权限暴露给前端一旦权限配置稍有不慎任何人都能读到或篡改数据。通过云函数收口所有读写逻辑都落在云端代码里前端拿到的只是接口返回的视图数据。2.4 页面与组件是如何组织的源码里如果按组件化思路组织首页一般会拆成三块顶部搜索栏、分类导航区、推荐菜谱列表。搜索栏是独立组件因为它在搜索页和首页都会被复用菜谱卡片也是独立组件用于首页、分类页、收藏页三处保证同一种视觉呈现只维护一份代码。app.json里注册的 tabBar 通常包含首页、分类、收藏、个人中心四个页签。选择这四个入口的理由很直接首页承载发现和推荐分类页解决目标明确的查询收藏页沉淀用户行为个人中心承载登录和账号信息。搜索功能从体验上不该做成 tab而是做成首页顶部的入口用户点进去再跳转到搜索页。页面间的跳转则遵循小程序后台栈的规则从首页进入详情页用wx.navigateTo详情页返回首页可以维持原页面状态如果在个人中心未登录状态下点收藏需要先弹登录引导这要求在收藏按钮的点击事件里做登录态判断而不是让请求发到云端才报错。3. 把「云开发菜谱小程序」跑通导入、建库、部署云函数的完整步骤3.1 跑通前的四个前置条件动手之前先把环境对齐避免中途卡在工具链上。第一需要一个微信小程序 AppID云开发不支持测试号注册小程序是免费的个人主体即可。第二安装微信开发者工具稳定版把它升级到最新稳定版能省去很多基础库兼容问题。第三机器上要有能解压 rar 的工具Windows 上我习惯用 BandizipmacOS 用 The Unarchiver这类工具能自动识别中文文件名编码直接用系统自带解压有可能出现乱码。第四微信开发者工具登录时确保扫码的微信号是该小程序项目的管理员或开发者否则导入后无法操作云开发。前置条件里最容易被忽略的是第四点。很多人拿着源码包导入后一片空白第一反应是代码有问题其实是因为扫码登录的微信没有这个项目的开发权限云开发环境根本不在当前账号名下。3.2 导入工程并把云环境 ID 对齐打开微信开发者工具选择「导入项目」目录指向解压后包含project.config.json的那一层填入小程序 AppID。导入完成后的第一件事不是点编译而是检查云环境 ID 是否一致。云环境 ID 是你在云开发控制台创建环境时生成的唯一标识格式类似cloud1-xxxx它同时出现在小程序端和云函数端。// miniprogram/app.js App({ onLaunch() { if (!wx.cloud) { console.error(当前基础库版本过低请使用 2.2.3 以上基础库); return; } wx.cloud.init({ env: your-env-id, // 替换成你自己的云环境 ID traceUser: true // 记录用户访问用于控制台分析 }); } });环境 ID 不一致的表现很典型编译不报错但所有云函数调用都在fail回调里返回环境不存在。修改时要检查两个位置app.js里的wx.cloud.init以及云函数代码里如果显式指定了env同样要替换。我个人习惯云函数里不写env让它默认运行在部署时指定的环境下这样换环境只需要在控制台切换部署目标。3.3 建集合与导入菜谱数据云开发控制台左侧「数据库」标签页里创建集合。菜谱项目至少需要这几个集合recipes菜谱主表、categories分类、favorites收藏、users用户信息首页如果有轮播图则再加一个banners。创建集合时不需要事先定义字段云数据库是文档型每条记录可以有不同的字段这对菜谱这种字段经常微调的业务很友好。导入菜谱数据有现成两条路。第一种是在控制台手动创建记录适合验证阶段只填三五条第二种是用 JSON 文件批量导入适合从其他平台迁移数据。批量导入时控制台对文件大小有限制我一般把数据切成每个文件几百条再分批导入避免一次导入失败整批回滚。// recipes.json 导入格式示例数组内每个对象是一条记录 [ { name: 可乐鸡翅, categoryId: meat, tags: [家常菜, 下饭, 甜口], cover: cloud://your-env-id.xxx/cover/kele-jichi.jpg, ingredients: [ { name: 鸡翅中, amount: 10个 }, { name: 可乐, amount: 330ml } ], steps: [ { text: 鸡翅两面煎至金黄, pic: }, { text: 倒入可乐没过鸡翅加生抽和老抽, pic: } ], cookTime: 30, difficulty: 1, views: 0, createTime: 2024-01-01T12:00:00.000Z } ]导入时注意_id字段不要自己填让系统生成否则后续详情页按_id查询时容易和已有的 ID 冲突。createTime建议用 ISO 字符串格式而不是时间戳因为控制台导出再导入时时间戳字段会被转成字符串格式一旦混用按时间排序就乱了。cover字段如果暂时没有云存储文件可以先填一个空字符串等图片传上去再更新不要因为它导致导入失败。3.4 部署云函数与真机预览数据到位后再部署云函数。在微信开发者工具的资源管理器里找到cloudfunctions目录对每个云函数子目录右键选择「上传并部署云端安装依赖」。这个选项会把该目录下的 Node.js 代码连同依赖一起打包上传到云端比「上传所有文件」更可靠因为依赖解析发生在云端不会因为本地 node_modules 版本不一致产生问题。部署完成后的验证路径是在开发者工具里编译项目 → 打开首页 → 看云函数列表里是否有调用记录。点开云开发控制台「云函数」页面能看到每次调用的耗时、返回状态和日志。凡是返回状态为失败的记录点开日志大概率能直接定位到错误行。真机预览这一步不能省。开发者工具里的模拟器环境和真机有三处关键差异扫码预览时使用的并非开发者工具内置的调试基础库、图片加载策略不同、手机网络下云函数冷启动耗时更长。点击开发者工具工具栏的「预览」按钮生成二维码用小程序管理员微信扫码在真机上完整走一遍首页 → 详情 → 收藏 → 个人中心的流程。真机上有问题再回来看工具优先排查上面三个差异点。4. 数据库设计与核心页面拆解收藏、搜索、自定义导航栏背后的逻辑4.1 集合设计五张表和字段选择菜谱小程序的数据库设计决定了后续所有功能的扩展空间。recipes是绝对的主表字段需要覆盖菜谱展示和检索的完整视图名称、分类 ID、标签数组、封面、食材数组、步骤数组、烹饪时长、难度、浏览量。steps内部每一项同时包含文字说明和可选的步骤图这个设计比把步骤拼成一个长字符串要灵活得多以后要做「分步高亮」或「步骤语音播报」时不用改表结构。categories表只需要三个字段分类名称、排序值、图标。排序值用于控制分类入口在首页的排列顺序菜品数量不需要冗余存储需要展示时可以动态聚合。favorites表是连接用户和菜谱的中间表核心字段是openid和recipeId用户维度查询时走openid菜谱热度统计时走recipeId。集合名主要字段用途权限建议recipesname, categoryId, tags, cover, ingredients, steps, cookTime菜谱主内容所有用户可读categoriesname, sort, icon分类导航所有用户可读favoritesopenid, recipeId, createTime收藏关系仅创建者可读写usersopenid, nickname, avatarUrl用户画像仅创建者可读写bannersimageUrl, link, sort首页轮播所有用户可读字段命名有个值得坚持的习惯所有时间字段统一用createTime、updateTime的 ISO 字符串不要混用create_date、cTime这类别名。开发的后期你会频繁写聚合统计统一命名能让查询逻辑少踩很多坑。4.2 列表接口分页与分类过滤的参数口径列表页是最容易写错的云函数因为分页参数和过滤条件一旦设计得不一致前端会频繁拿到重复或缺失的数据。常见的做法是把列表云函数设计成统一入口通过categoryId控制分类过滤通过page和pageSize控制分页返回数据里附带total方便前端渲染触底加载。// cloudfunctions/getRecipes/index.js const cloud require(wx-server-sdk); cloud.init(); const db cloud.database(); const recipes db.collection(recipes); exports.main async (event) { const { categoryId , page 1, pageSize 10, keyword } event; const where {}; if (categoryId) { where.categoryId categoryId; } if (keyword) { // 用正则做菜名模糊匹配仅适用于中小数据量 where.name db.RegExp({ regexp: keyword, options: i }); } const query recipes.where(where); const countRes await query.count(); const listRes await query .orderBy(createTime, desc) .skip((page - 1) * pageSize) .limit(pageSize) .get(); return { total: countRes.total, list: listRes.data, page, pageSize }; };分页这里有两个关键参数skip表示跳过的记录数limit表示最多返回多少条。在小数据量下skip limit是够用的但skip的数字越大查询越慢因为数据库要扫描并丢弃前面所有记录。菜谱项目上千条数据以内感觉不到差别真做到数万条时要改成基于createTime和_id的游标分页下一页查询时带上createTime 上一页最后一条记录的时间条件。db.RegExp是云数据库的查询语法服务端执行正则匹配适合搜索菜名和标签。它和普通正则的区别是必须在db.RegExp里传参数直接把 JavaScript 字符串拼进where是查不出结果的。搜索接口不建议对每个词都做正则比如用户输入「鸡翅」时命中菜名即可标签搜索可以另建搜索索引或者简单点直接把tags数组的匹配也放进同一条件。4.3 收藏功能客户端权限和云函数身份的边界收藏功能前后端各有一次身份判断。页面端判断用户是否已登录未登录则弹窗引导去个人中心授权云函数端判断「当前用户收藏过没有」决定执行插入还是删除。这里有一个关键区别云函数运行时的身份不是用户身份而是管理员身份。云函数里直接查favorites.where({ openid: xxx })能查到所有用户的收藏记录不受集合权限限制但云函数本身并不知道调用者是谁必须显式从上下文里取。// cloudfunctions/collectRecipe/index.js const cloud require(wx-server-sdk); cloud.init(); const db cloud.database(); const favorites db.collection(favorites); exports.main async (event) { const { OPENID } cloud.getWXContext(); // 从云函数上下文取 openid const { recipeId, action } event; // action: add 或 remove if (action add) { const existed await favorites.where({ openid: OPENID, recipeId }).get(); if (existed.data.length 0) { return { code: 0, message: 已收藏过 }; } await favorites.add({ data: { openid: OPENID, recipeId, createTime: new Date().toISOString() } }); return { code: 0, message: 收藏成功 }; } if (action remove) { await favorites.where({ openid: OPENID, recipeId }).remove(); return { code: 0, message: 已取消 }; } };这里最容易翻车的点是直接用wx.cloud.database()在客户端写收藏。客户端的权限模型是「仅创建者可读写」时查询条件里必须带_openid字段才能命中自己的记录如果权限误设成「所有用户可读写」那任何用户都能删掉别人的收藏。所以收藏这类写操作尽量走云函数权限可以直接设为「仅管理端可读写」从入口上封死。收藏状态在详情页需要回显。我的做法是进入详情页时并行调用两个云函数getRecipeDetail拿菜谱数据、collectRecipe的查询模式拿当前用户的收藏状态。收藏状态不要单独存在本地缓存里因为用户可能在多台设备上登录缓存会导致收藏按钮显示与实际不符。4.4 自定义顶部导航栏的高度计算菜谱详情页如果想做成沉浸式顶图效果——封面图延伸到状态栏后面就需要自定义导航栏。自定义导航栏的难点不在视觉而在精确拿到「状态栏高度」和「胶囊按钮位置」。// miniprogram/pages/detail/index.js const { windowInfo, menuButton } getNavigationBarInfo(); function getNavigationBarInfo() { const windowInfo wx.getWindowInfo(); // 状态栏高度、屏幕宽度等 const menuButton wx.getMenuButtonBoundingClientRect(); // 胶囊按钮位置 const statusBarHeight windowInfo.statusBarHeight; const navBarHeight (menuButton.top - statusBarHeight) * 2 menuButton.height; return { statusBarHeight, navBarHeight, menuButton }; }导航栏的高度计算公式是业界通用的状态栏高度 胶囊按钮上下留白等于胶囊到状态栏的距离 × 2 胶囊高度。直接写死一个44px是典型的翻车做法因为不同机型的胶囊按钮位置不一致刘海屏和非刘海屏的状态栏高度差异很大。拿到statusBarHeight后要把它应用到页面的padding-top把主内容区顶到状态栏下方同时给自定义导航栏里的返回按钮和标题设置top: statusBarHeight避免被刘海遮挡。顶部导航栏高度相关的适配要在真机预览时重点检查。开发者工具模拟器默认的机型参数和真机总有偏差建议至少用一台刘海屏手机和一台非刘海屏手机交叉验证。还有一点容易忽视用户从详情页转发小程序给朋友时转发卡片会复用当前页面的截图如果导航栏位置错位转发的卡片图会非常显眼。5. 实践避坑云开发版菜谱小程序的 5 个高频翻车现场5.1 图片资源把主包推到 2612KB编译直接报 2MB 超限现象代码写好后一点编译控制台报source size 2612kb exceed max limit 2mb小程序包根本生成不出来。原因本地资源目录里塞了大量菜品图片或者代码里用了 base64 格式的内嵌小图导致主包体积超出微信限制。小程序主包限制是 2MB这不是配置项是平台硬性约束。图片是菜谱类项目里最占体积的资源一张 800×600 的 JPG 就有 100-200KB本地塞二十张就撞线。解决把图片全部从代码包里挪走传云存储页面里用cloud://协议的 fileID 引用。云存储本身不占主包体积代码包只留文本数据。打包时在project.config.json里确认packOptions.ignore规则把images等本地资源目录排除出去。如果一个页面确实需要极快的首图展示最多挑一两张 10KB 以内的占位图放本地其余全部云端化。5.2 开发者工具能看到图片真机上一片空白现象模拟器上菜谱封面、步骤图显示正常一扫码真机预览图片区域空白有时候过几秒又自动加载出来加载时间不固定。原因页面里把云存储的图片转成了https://临时链接再放到image组件的src上。云存储生成的临时链接有时效性开发者在工具里调试时链接有效真机上如果代码把临时链接缓存起来过期后就加载失败。另外开发者工具默认不校验域名真机上对非白名单域名有严格限制临时链接的域名不在合法域名列表里时会静默失败。解决image组件的src直接使用cloud://协议的 fileID微信内置的图片组件能识别并自动拉取。如果业务确实需要https链接调用wx.cloud.getTempFileURL换取临时链接后立即使用不要存到变量里复用。排查时打开真机调试的 Network 面板看图片请求的状态码404或403都是临链失效的典型特征。5.3 云函数首次调用超时列表页 3 秒转圈现象首页第一次加载时列表空白转圈超过 3 秒才出数据退出小程序重新进入第二次加载明显快很多。原因云函数存在冷启动机制。首次调用时云端要拉起运行时并加载依赖耗时通常比后续调用多几倍。菜谱列表函数里如果包含了多个await串行查询——先查分类、再查菜品、再查轮播图——整体耗时叠加后就容易触发云函数的默认超时时间。解决在云开发控制台「云函数 → 配置」里把超时时间从默认值调整为 20 秒给冷启动留出余量。同时优化云函数内部逻辑把互不依赖的查询用Promise.all并行执行。改动代码后要注意只有重新「上传并部署」才会让配置和新代码同时生效只改控制台配置不重新部署逻辑不变超时依然在旧代码上生效。5.4 权限开成「所有用户可读写」收藏表被人恶意刷爆现象小程序上线后favorites集合里出现大量非真实用户的收藏记录数据量短时间内暴涨账单费用异常。原因数据库权限设置里把favorites集合点成了「所有用户可读写」。客户端 API 在权限宽松时可以被外部脚本绕过页面直接调用任何人拿到env后都能往集合里写入任意数据包括伪造的收藏关系。解决所有涉及写操作的集合权限一律收紧。favorites、users这类带隐私语义的集合设为「仅管理端可读写」所有读写都走云函数云函数以管理员权限运行不受集合权限限制。recipes、categories这类公开内容设为「所有用户可读」但不可写。这样配置后即使前端被逆向能做的也只有读取公开菜谱破坏面被控制住了。5.5 模拟器里能登录真机上拿不到手机号现象开发者工具模拟器里点「获取手机号」按钮可以正常弹出授权并拿到号码真机预览时按钮点击后没有反应或者云函数报错解析不到数据。原因手机号获取接口是微信官方的高敏感能力前提是已认证的小程序 真机环境 用户主动点击 button 触发。开发者工具模拟器无法完整模拟手机号授权链路部分源码里用模拟器测试时走了临时放行的逻辑真机上这个逻辑不生效。另一个常见原因是没有把getPhoneNumber的返回数据传到云函数端用cloud.getOpenData解析而是试图在小程序端直接解密解密所需的 session_key 根本拿不到。解决页面里用button open-typegetPhoneNumber bindgetphonenumberonGetPhone来触发回调里拿到detail.code后传给云函数。云函数内调用cloud.getOpenData({ code })换取真实手机号这一步必须发生在云端客户端不能解密。真机和模拟器行为不一致属于平台限制不要试图在模拟器上验证整条链路直接扫码真机测。6. 验证与进阶把首屏压进 1 秒再跑通「猜你想做」推荐6.1 首屏性能并行请求、懒加载与骨架屏验证一个菜谱小程序是否「能打」第一个指标是首屏加载耗时。打开开发者工具的「性能」面板真机预览状态下点开录制看首页index页面从启动到首帧渲染的时间线。常见瓶颈集中在三处云函数串行调用、图片并发请求过多、页面数据未到达时白屏。性能优化按优先级做三件事第一首页的数据请求合并成一个云函数内部用Promise.all同时查轮播图、分类、推荐列表把三次网络往返压成一次第二列表图片启用懒加载image组件加lazy-loadtrue屏幕外的卡片不参与加载第三云函数返回前把封面图缩略图化云存储支持图片处理参数小程序端只加载裁剪后的缩略图详情页再加载原图。缩略图的格式类似在 fileID 后拼接处理参数具体参数名以云存储文档为准但思路是「列表用小图详情用大图」这能显著减少首屏流量。6.2 用漏斗数据验证菜谱功能是否真的可用首屏快不等于产品成立。我习惯在recipes集合里维护views字段并在用户进入详情页时调用云函数自增同时在收藏表建立后通过云开发控制台的数据库聚合分析看三个比率详情页浏览量中点击收藏的比例、搜索页到详情页的转化率、收藏后七天内再次访问的比例。如果数据量很小直接在控制台用db.collection(favorites).count()就能知道有多少人收藏过数据量上来后看「用户访问来源」更有效——是通过首页推荐进入详情多还是分类页进入多。菜谱类内容的核心指标是「收藏率」我见过很多项目浏览量高但收藏率不到 1%这种情况要回头检查详情页菜谱质量、封面吸引力和步骤清晰度而不是继续优化加载速度。一个反常识结论是菜谱小程序的重度用户不看推荐流他们直接搜索菜名。所以搜索页的接口耗时和结果准确性权重比首页推荐更高。6.3 基于收藏标签的「猜你想做」冷启动当收藏数据积累到几百条后可以做最简单的推荐统计当前用户所有收藏菜谱的标签频率把频率最高的前三个标签作为特征从全量菜谱里找出包含这些标签但用户还没收藏过的菜品。这个方案不需要任何机器学习依赖一个云函数就能实现。// cloudfunctions/recommendRecipes/index.js const cloud require(wx-server-sdk); cloud.init(); const db cloud.database(); const _ db.command; exports.main async () { const { OPENID } cloud.getWXContext(); // 1. 查当前用户收藏的菜谱 ID const favRes await db.collection(favorites) .where({ openid: OPENID }) .limit(50) .get(); if (favRes.data.length 0) { // 没有收藏记录时回退到热门榜单 const hot await db.collection(recipes) .orderBy(views, desc) .limit(6) .get(); return { list: hot.data }; } const favIds favRes.data.map(item item.recipeId); // 2. 取收藏菜谱的标签并统计 const favRecipes await db.collection(recipes) .where({ _id: _.in(favIds) }) .get(); const tagCount {}; favRecipes.data.forEach(r { (r.tags || []).forEach(t { tagCount[t] (tagCount[t] || 0) 1; }); }); const topTags Object.keys(tagCount) .sort((a, b) tagCount[b] - tagCount[a]) .slice(0, 3); // 3. 根据标签召回排除已收藏 const recommend await db.collection(recipes) .where({ tags: _.in(topTags), _id: _.nin(favIds) }) .limit(10) .get(); return { list: recommend.data }; };这个方案的边界要认得清楚它适合几百到几千条菜谱的冷启动阶段推荐结果的多样性基本没有用户连续收藏几道红烧肉后推荐流里大概率也都是红烧类。做到这个程度后想再进一步需要给用户建偏好画像集合记录浏览时长、搜索关键词、收藏时间分布把标签加权改成带时间衰减的评分模型。但说实话对大部分菜谱小程序来说把首页排版、菜谱质量、搜索准确度打磨好比上一个复杂推荐模型带来的实际收益更大。我在这类项目里踩过最深的一次坑是把所有图片堆在本地上线前一天才发现主包超限连夜传云存储才救回来。后来养成一个习惯任何新的菜谱图片第一时间进云存储代码里只保留 fileID 引用。这个习惯省下的时间比我写两套云函数还多。希望这些经验能帮到你把云开发版菜谱小程序从解压 rar 一路跑到真实用户手里。本文还有配套的精品资源点击获取