ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信小程序地方美食分享系统开发全流程:从云开发到上线

微信小程序地方美食分享系统开发全流程:从云开发到上线 接了个帮忙指导的项目题目是《基于微信小程序的地方美食分享设计与实现任务书》。拿到手翻了几页任务书写得挺标准用户登录、美食发布、分类浏览、地图定位、评论点赞功能一栏一栏列得很清楚但真到动手开发的时候光是一个图片上传怎么和帖子绑定就够卡大半天的。这类题目在毕设里出现频率很高和社区团购、宠物寄养、生鲜配送同属“微信小程序某个垂直领域”的万能模板。今天我想把这套东西从需求拆解、技术选型、数据库设计到上线审核按我自己带项目的顺序完整过一遍适合正在写任务书、准备开题或者已经写了代码想查漏补缺的人。1. 从任务书到落地这个项目到底在解决什么问题1.1 任务书里的“地方美食分享”拆开看任务书一般不会直接告诉你“怎么做”它只会写目标开发一个基于微信小程序的地方美食分享平台用户能浏览美食信息、发布探店内容、查看位置、发表评论。这句话里真正值钱的三个词是“地方”、“美食”、“分享”把这三个词翻译成技术方案整个项目的主线就出来了。先说“地方”。这不是简单地在数据库里存一个城市名它意味着你需要处理用户当前位置、地图选点、POI名称、经纬度甚至按距离排序。真机上调用wx.getLocation、wx.chooseLocation都涉及用户隐私授权在公众平台配置接口权限这部分很多人第一次做时会一脸懵。再说“美食”。它不只是标题和几张图还需要分类、人均价格、营业时间、推荐理由、位置地址以及封面图和多图。你要想清楚哪些字段在列表页展示哪些只在详情页展示哪些字段需要冗余存储这都是数据库设计阶段要考虑的。最后是“分享”。这是典型的UGC用户生成内容场景意味着你要处理登录身份、内容发布、内容审核、敏感词过滤、点赞收藏评论还要考虑违规内容的下线操作。这三个词拆开以后任务书里的“功能需求”一段几乎可以直接拿来用。1.2 为什么选微信小程序而不是App或公众号很多答辩老师会问为什么用微信小程序这个问题如果你答不好前面功能做得再好也会被认为“为了交作业而选型”。你得从场景和成本两个角度回答。本地美食分享是典型的“低频刚需”场景用户不会为了找附近好吃的专门下一个App。微信小程序扫码即开用完即走符合美食探店这种即时需求。公众号虽然也能做内容但交互能力受限无法直接调起地图选点、无法在图文里做复杂表单、无法实现流畅的页面栈跳转。App开发周期长、分发成本高对个人项目和教学项目来说根本没必要。另外小程序和微信账号体系天然打通用户不需要重新注册这直接砍掉了最繁琐的登录流程。再加上微信生态里的“附近的小程序”、搜索入口、订阅消息都方便做二次触达。当然也要承认小程序有包体积限制和平台规则约束但在这个题目里优势远远大于劣势。1.3 目标用户与核心场景任务书里通常会要求画用例图你不能只画一个“用户”角色。我把这个项目拆成四类角色本地食客、外地游客、美食分享者、管理员。他们的核心诉求完全不同。用户角色核心诉求对应功能本地食客知道身边有什么好吃的快速决策首页信息流、分类筛选、地图定位、详情查看外地游客到陌生城市找特色美食城市切换、关键词搜索、详情页位置导航美食分享者记录探店体验分享给同好发布图文、管理动态、互动评论管理员保证内容质量和社区秩序帖子状态管理、内容审核、下架处理一个典型闭环是这样的用户周六中午打开小程序定位到自己所在城市在首页按“小吃”分类刷到附近一条探店帖点进去看到店名、人均价格和用户评价点了收藏然后自己也拍了几张照片发了一条动态。这个场景把首页、详情、发布、个人中心全部串起来了第一版只要把这条链路跑通功能上已经能交差。2. 功能模块怎么拆才不会做着做着跑偏2.1 核心功能模块清单我见过很多人在任务书里把功能写得特别多真正实现时发现做不完。正确的做法是先列功能点再标优先级砍掉非核心功能。下面这个清单是常见任务书里要求的我标注了建议的优先级。模块具体功能优先级用户模块微信登录、头像昵称设置、我的资料高内容浏览首页信息流、分类筛选、关键词搜索高发布管理图文上传、位置选择、发布/删除高互动模块点赞、评论、收藏中位置服务当前定位、地图选点、距离排序中管理后台帖子状态管理、违规内容下线低中第一版建议不要做编辑功能删除做了都很勉强。如果任务书里明确写了“编辑美食分享”可以做一个简单版本只能编辑未通过审核的帖子。实际上在毕设答辩场景“功能闭环完整”比“功能数量多”更关键。你有首页、发布、详情、互动、个人中心加上一个管理员下架入口已经能讲一个完整的故事。2.2 页面路由与底部Tab设计页面规划直接影响你后面写代码的心智负担。我建议按下面这套页面来安排pages/index/index首页信息流承载分类筛选和进入详情pages/detail/detail美食详情图文、位置、互动按钮pages/publish/publish发布页图片上传、表单填写、位置选择pages/mine/mine个人中心我的发布、收藏、管理入口pages/search/search搜索页按关键词搜索pages/city/city城市切换页按城市过滤内容底部Tab只放三个首页、发现、我的。其中“发现”可以放城市切换、分类推荐、排行榜也可以直接合并到首页。我的建议是TabBar为首页、发现、我的发布按钮不要做成Tab而是放在首页底部悬浮位置通过wx.navigateTo跳转到发布页。为什么发布按钮不放在Tab里因为发布是低频且需要沉浸式操作的动作放在Tab中会占用一个宝贵的首屏位置切换Tab时页面还会缓存弹窗状态体验很割裂。用悬浮按钮加独立页面发布完回到首页时在onShow里刷新列表是最稳的方案。如果你想模仿大众点评那种中间凸起的自定义TabBar新手不太建议碰cover-view在不同机型上的兼容问题能让你调一天。2.3 数据流转关系页面规划清楚以后数据流转关系基本就定了。完整链路是用户进入小程序前端调用login云函数通过微信上下文拿到openid在users集合里查询或创建用户记录首页发起getFoodList云函数请求数据库按分页返回帖子列表用户点击某张卡片进入详情页getFoodDetail云函数一次性返回帖子详情、作者信息、当前用户是否点赞收藏发布页先上传图片到云存储拿到fileID数组后再调用createFood云函数写入数据库管理员在管理页面调用updateFoodStatus修改帖子状态。这条链路里最容易忽略的是“云函数之间不要互相调用”。简单项目里每个云函数直接操作数据库就够了不要为了代码复用而用callFunction去调用另一个云函数那样增加一次网络往返出了错还不好排查。2.4 管理员后台的取舍任务书里如果写了“后端管理”千万别慌着搭一个Vue后台。小程序项目最好的做法是做一个轻量的管理页面。你在users集合里给指定openid设置role: admin个人中心检测到管理员身份时显示“管理入口”管理页面按状态筛选帖子支持通过、驳回、下线三个操作。这个方案不用单独增加管理系统页面代码量也不大但能让答辩老师看到完整的“内容审核闭环”。如果实在没时间做页面可以直接在云开发控制台修改status字段但演示效果会差很多。我见过太多人拿着控制台截图讲“这是后台管理”老师问一句“那你怎么下架违规内容”就卡住了。做一个只包含筛选和状态修改的管理页比再做一套登录后台划算得多。3. 技术选型和环境准备微信小程序、云开发、原生三选一3.1 原生小程序 vs uni-app vs Taro怎么选很多人在技术选型上纠结其实只要想清楚一个问题你是不是需要多端发布方案优势劣势适合场景原生微信小程序官方文档全、API调用直接、调试方便只能跑微信不能一套代码多端任务书明确“基于微信小程序”的题目uni-appVue语法、可编译多端、生态丰富平台差异编译问题多插件质量参差你想同时发H5/App且有Vue基础TaroReact语法、可多端学习成本不低小程序原生能力封装有延迟你熟悉React且想走跨端路线如果你是照着任务书做毕设我建议选原生。原因很直接本项目聚焦微信生态原生可以减少底层兼容问题不用为了“会不会被说技术栈旧”而强行上跨端框架。答辩老师如果问“技术含量是不是太低”你可以回答用最合适的工具把闭环做扎实后期如果需要扩展再考虑跨端框架这是工程上的取舍。3.2 后端方案微信云开发还是自建服务器后端是整个项目最容易被小看的点。任务书里可能只写“服务端接口”但你要决定是用云开发还是自己搭服务器。微信云开发的优点非常明显数据库、云存储、云函数三件套都是微信团队维护好的免去了域名备案、HTTPS证书、服务器部署的麻烦。云函数里可以直接拿到用户openid不用自己写登录接口代码量和运维成本能省一半。自建服务器当然可控性更强能自由选Node、Java、Python也能接更多第三方服务但你需要买服务器、配HTTPS、处理安全问题这在毕设周期里是很大的时间黑洞。如果是我这个项目毫不犹豫选云开发。免费额度对学生项目完全够用测试阶段基本不会产生费用。但是要提醒一句云开发不等于不需要写服务端逻辑你的查询、校验、数据聚合依然要写在云函数里这部分才真正体现你的后端能力。3.3 开发者工具配置与项目初始化环境准备看起来简单但初期卡住的人不在少数。我按步骤说。第一步下载微信开发者工具用你的微信扫码登录。第二步注册小程序账号拿到AppID。不要用游客模式的测试号因为测试号无法申请地理位置等开放接口等你做到地图定位才发现就晚了。第三步在开发者工具里新建项目选择“不使用模板”填好AppID。第四步点击工具栏的“云开发”按钮创建云环境记下环境ID。第五步在app.js初始化云能力// app.js App({ onLaunch() { if (!wx.cloud) { console.error(请使用 2.2.3 以上基础库以使用云能力); return; } wx.cloud.init({ env: your-env-id, // 替换成你的云环境ID traceUser: true }); this.globalData {}; } });这里有个最典型的坑env填错或者没填开发者工具里云函数可能能用但真机上调用会一直失败。如果你有多个云环境一定要搞清哪个是开发环境、哪个是正式环境我把环境ID直接写在app.js顶部注释里方便自查。3.4 目录结构与代码分层项目结构建议这样组织不要所有页面都堆在根目录下miniprogram/ pages/ index/ detail/ publish/ mine/ manage/ components/ food-card/ empty/ utils/ cloud.js util.js images/ cloudfunctions/ login/ getFoodList/ getFoodDetail/ createFood/ updateFoodStatus/原生小程序的页面是四个文件一组js/json/wxml/wxss一个页面一个文件夹是规矩不要打破。云函数独立放在cloudfunctions目录下每个云函数是一个独立部署单元有自己独立的package.json。这样的好处是上传部署时互不影响坏处是公共逻辑不能共享所以简单项目里公共代码直接复制一份到每个云函数就行别为了抽公共库把部署流程搞复杂。4. 数据库设计美食内容的核心字段与集合规划4.1 用户集合 users用户集合是整个系统的起点。字段不需要太多字段类型说明_idstring自动生成openidstring微信用户唯一标识云函数获取nickNamestring昵称avatarUrlstring头像fileIDrolestringuser / admincitystring默认城市createTimenumber创建时间updateTimenumber更新时间关于权限云开发数据库的安全规则一定要设置否则别人能随意改你的用户资料。users集合建议设置“所有用户可读仅创建者可写”云函数因为拥有管理员权限不受这个限制。真机调试时发现报错“collection not found”或“permission denied”十有八九是安全规则没配好。在云开发控制台“数据库-权限设置-自定义安全规则”里可以根据官方模板改。4.2 美食帖子集合 dishes帖子集合是整个项目最核心的数据结构字段设计决定了建页面时顺畅还是痛苦。字段类型说明titlestring标题contentstring探店描述coverImagestring封面图fileIDimagesarray图片fileID数组最多9张categorystring分类小吃/正餐/甜品/饮品/夜宵citystring城市名locationobject{name, address, latitude, longitude}avgPricenumber人均价格authorIdstring关联users._idauthorNamestring作者昵称冗余authorAvatarstring作者头像冗余likesnumber点赞数commentsnumber评论数viewsnumber浏览量statusstringpending/published/rejected/offlinecreateTimenumber创建时间updateTimenumber更新时间列表页只取作者和头图详情页才查完整正文。冗余authorName和authorAvatar是为了列表页不需要再去查users集合避免一次页面请求要查两张表。等数据量大了在控制台对createTime、category city建立索引否则按城市分类查询会越来越慢。4.3 评论、点赞、收藏三张表的处理点赞和收藏最怕数据重复。云开发数据库不支持唯一索引所以我建议把文档ID直接设计成拼接字符串点赞表likes的文ID用${openid}_${dishId}这样同一个用户对同一篇帖子只能存在一条记录查询时用doc直接拿不需要where加count。// 判断当前用户是否已点赞 const likeRes await db.collection(likes) .doc(${openid}_${dishId}) .get() .catch(() null); if (likeRes) { // 已点赞 } else { // 未点赞 }评论表单独存放dishId、userId、content、createTime。每次评论成功后用db.command.inc把dishes文档里的comments字段加一删除时减一。不要试图把评论数组直接嵌在帖子文档里数组长度会有上限而且并发追加时可能出现数据错乱。4.4 媒体文件存储与安全规则图片用的是云存储不是数据库。发布时前端把图片传到云存储返回fileID再把fileID数组写进dishes文档。云存储需要配置安全规则建议用{ read: true, write: auth ! null }read设为true是因为图片要给所有用户展示write设为登录用户可写避免匿名用户乱传文件。云存储的fileID在小程序端可以直接放在image组件的src里显示不需要额外转临时链接。路径命名建议带上用户标识和时间戳比如dish/${openid}_${Date.now()}_0.jpg防止同名覆盖。5. 核心页面实现信息流、发布、详情、个人中心全拆解5.1 首页信息流分页加载与下拉刷新首页是用户看到的第一屏核心操作是列表分页加载。这里特别要注意“加载更多”的实现位置小程序有专门的onReachBottom钩子页面滚动到底部自动触发不需要自己判断滚动距离。Page({ data: { list: [], page: 0, pageSize: 10, finished: false, loading: false }, async loadList(reset false) { if (this.data.loading) return; if (reset) { this.setData({ page: 0, finished: false, list: [] }); } const page this.data.page 1; this.setData({ loading: true }); try { const res await wx.cloud.callFunction({ name: getFoodList, data: { page, pageSize: this.data.pageSize, category: this.data.category } }); const newList res.result.data || []; this.setData({ list: this.data.list.concat(newList), page, finished: newList.length this.data.pageSize, loading: false }); } catch (e) { this.setData({ loading: false }); wx.showToast({ title: 加载失败, icon: none }); } }, onReachBottom() { if (!this.data.finished) this.loadList(); }, onPullDownRefresh() { this.loadList(true).finally(() wx.stopPullDownRefresh()); } });这个代码片段有几个关键点loading锁防止重复触发finished判断服务器返回是否小于pageSize小于说明没有更多下拉刷新时reset为true会清空列表重新从第1页加载。页面json里要记得开启{ enablePullDownRefresh: true, backgroundTextStyle: dark, onReachBottomDistance: 50 }开发者在工具里有时onReachBottom触发不灵敏切到真机预览基本正常。如果你的列表页还加了分类Tab点分类切换时要重置分页参数否则会出现新分类下还带着旧分类的页数。5.2 发布页图片上传、表单校验、提交状态处理发布页流程是这样的选择图片、上传云存储、选择位置、填表单、提交云函数。其中最容易出错的顺序是必须先上传图片拿到fileID再提交云函数。千万不要把本地临时路径直接写进数据库因为临时路径过期后图片就加载不出来了。async chooseImages() { const res await wx.chooseMedia({ count: 9 - this.data.images.length, mediaType: [image], sizeType: [compressed], sourceType: [album, camera] }); this.setData({ images: this.data.images.concat(res.tempFiles.map((f) f.tempFilePath)) }); } async submit() { if (this.data.isSubmitting) return; // 这里做表单校验标题非空、至少一张图、已选分类 this.setData({ isSubmitting: true }); wx.showLoading({ title: 发布中..., mask: true }); try { const uploaded []; for (let i 0; i this.data.images.length; i) { const cloudPath dish/${Date.now()}_${i}_${Math.floor(Math.random() * 1000)}.jpg; const res await wx.cloud.uploadFile({ cloudPath, filePath: this.data.images[i] }); uploaded.push(res.fileID); } await wx.cloud.callFunction({ name: createFood, data: { title: this.data.form.title, content: this.data.form.content, category: this.data.form.category, avgPrice: Number(this.data.form.avgPrice), location: this.data.form.location, images: uploaded, coverImage: uploaded[0] } }); wx.hideLoading(); wx.showToast({ title: 发布成功 }); setTimeout(() wx.navigateBack(), 1500); } catch (e) { wx.hideLoading(); wx.showToast({ title: 发布失败请重试, icon: none }); this.setData({ isSubmitting: false }); } }这里的图片上传我特意用了for循环而不是Promise.all。9张图并发上传在真机弱网环境下大概率会失败串行上传虽然慢一点但稳定性好很多。上传过程的体验问题可以通过进度提示弥补比如显示“已上传3/9”而不是让用户干等。选择位置用wx.chooseLocation它会返回name、address、latitude、longitude正好对应数据库里的location对象。表单里的分类可以用radio-group实现就是最简单的“单选”控件人均价格用input typedigit提交时转成数字防止字符串拼进统计。5.3 详情页富文本展示与关联数据查询详情页最忌讳的做法是前端发起两三个请求分别拉帖子、作者、点赞状态然后再分步渲染。一个云函数搞定所有查询能减少网络请求次数也更好维护。// cloudfunctions/getFoodDetail/index.js exports.main async (event) { const { id } event; const db cloud.database(); const _ db.command; const dishRes await db.collection(dishes).doc(id).get(); const dish dishRes.data; const { OPENID } cloud.getWXContext(); let isLike false; let isFavorite false; try { await db.collection(likes).doc(${OPENID}_${id}).get(); isLike true; } catch (e) {} try { await db.collection(favorites).doc(${OPENID}_${id}).get(); isFavorite true; } catch (e) {} await db.collection(dishes).doc(id).update({ data: { views: _.inc(1) } }); return { code: 0, data: { dish, isLike, isFavorite } }; };注意这里我没有再查作者信息因为dishes表里已经冗余了authorName和authorAvatar。如果想拿作者最新的头像昵称才需要额外查一次users集合。详情页底部放“踩一脚”或“想去”的按钮点击后调用likeFood云函数内部先判断likes文档是否已存在再决定增删并同步dishes表的计数。详情页还有一个很容易出彩的点接入地图导航。展示location.name和location.address再放一个“打开地图”按钮调用wx.openLocation传经纬度和店名用户就能直接导航过去。这个功能对地方美食项目来说几乎必做而且代码不多。5.4 个人中心我的发布、收藏、评论管理个人中心是用户管理和数据资产页面。我建议顶部显示头像、昵称、城市下面用两个Tab切换“我的发布”和“我的收藏”。我的发布逻辑调用myFoods云函数按authorId为当前用户查询dishes集合分页返回每张卡片带status标签已发布、审核中、已驳回。我的收藏逻辑先查favorites集合拿到当前用户收藏的dishId数组再用db.command.in一次性查出这些帖子的详情。// 云函数myFavorites const db cloud.database(); const _ db.command; const { OPENID } cloud.getWXContext(); const favRes await db.collection(favorites) .where({ openid: OPENID }) .limit(100) .get(); const dishIds favRes.data.map((f) f.dishId); const dishRes await db.collection(dishes) .where({ _id: _.in(dishIds), status: published }) .get(); return { code: 0, data: dishRes.data };收藏数量多时注意in操作符对数组长度有上限第一版十几条数据完全没问题。个人中心的管理入口在这里判断role admin普通用户不显示。6. 请求层与登录态云函数和前端如何配合6.1 云函数统一入口别到处硬编码很多新手写页面时直接在每个方法里写wx.cloud.callFunction这样做不是不行但改一处云函数名称就要全局搜索替换效率很低。我习惯封装一个cloud.js// utils/cloud.js function callFunction(name, data {}, options {}) { const { loading false, loadingTitle 加载中 } options; if (loading) wx.showLoading({ title: loadingTitle, mask: true }); return wx.cloud.callFunction({ name, data }) .then((res) { if (loading) wx.hideLoading(); const { result } res; if (result result.code 0) { return result.data; } wx.showToast({ title: (result result.message) || 操作失败, icon: none }); return Promise.reject(result); }) .catch((err) { if (loading) wx.hideLoading(); wx.showToast({ title: 网络异常, icon: none }); return Promise.reject(err); }); } module.exports { callFunction };封装的约定是所有云函数统一返回{ code: 0, data, message }。前端判断code不是0就toast错误。这样页面里只需要写const data await callFunction(getFoodList, { page: 1, pageSize: 10 });不需要在每个页面里重复写if (err) wx.showToast。这里的loading选项用于发布、登录这种需要阻塞用户操作的场景列表页加载更多就不要开全屏loading容易打断滚动。6.2 云函数内部公共模块怎么处理云函数目录下每个函数是独立部署的代码不能像小程序端那样直接import兄弟目录。我见过有人把公共数据库操作抽成cloudfunctions/common结果云函数部署时找不到文件夹折腾半天。最简单的方案是把公共的初始化代码复制到每个云函数的index.js顶部。对地方美食分享这种体量的项目公共代码也就是两三行云初始化复制带来的维护成本几乎可以忽略。如果你实在想抽可以研究一下云函数分层Layer但第一版没必要。6.3 登录态与用户身份识别云开发登录态的底层逻辑一定要理解小程序前端可以拿到openid吗拿不到必须通过云函数。云函数里cloud.getWXContext()返回的OPENID才是可信身份前端传过来的任何openid字段都不能信。// cloudfunctions/login/index.js const cloud require(wx-server-sdk); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db cloud.database(); exports.main async (event) { const { OPENID } cloud.getWXContext(); const { nickName, avatarUrl } event; const userRes await db.collection(users).where({ openid: OPENID }).get(); let user; if (userRes.data.length) { user userRes.data[0]; await db.collection(users).doc(user._id).update({ data: { nickName, avatarUrl, updateTime: Date.now() } }); } else { const addRes await db.collection(users).add({ data: { openid: OPENID, nickName, avatarUrl, role: user, createTime: Date.now(), updateTime: Date.now() } }); user { _id: addRes._id, openid: OPENID, nickName, avatarUrl }; } return { code: 0, data: user }; };登录后建议把用户_id放到globalData里个人中心和发布页直接从全局读取避免每个页面重复调用login。如果用户第一次进小程序直接去发帖login还没执行完会出现“找不到用户”的问题。我通常在publish云函数里再查一次当前OPENID对应的用户如果不存在就先自动创建前端不用管。6.4 错误处理与加载状态错误处理最烦的是wx.showLoading和wx.showToast并存。发布成功时你如果先hideLoading再showToast理论上没毛病但两个弹窗动画交替容易让toast一闪而过。稳妥做法是hideLoading之后延迟几十毫秒再showToast或者直接用wx.showToast的mask属性替代loading。云函数返回的异常分两层网络层异常和业务层异常。网络层异常常见的是“cloud.callFunction:fail”这种前端网络问题不需要给用户看错误详情统一提示“网络异常请重试”。业务层异常比如“内容包含违规词”这种应该展示具体原因。所以在封装里网络错误与HTTP状态码类似业务错误通过code区分这样前端处理起来非常清晰。7. 我被问倒过的坑定位、上传、SetData与真机差异7.1 头像昵称获取的坑老教程已经失效如果你搜教程会发现大量文章还在教wx.getUserProfile但真机上这套方法已经拿不到用户真实头像和昵称了只会返回“微信用户”和默认头像。微信现在推荐的是头像昵称填写能力头像用button的open-typechooseAvatar昵称用input typenickname。button open-typechooseAvatar bind:chooseavataronChooseAvatar选择头像/button input typenickname bindchangeonNicknameChange placeholder请输入昵称 /头像选中的结果是临时路径不能直接写数据库要先通过wx.cloud.uploadFile传到云存储拿到fileID再保存。这个坑特别隐蔽很多人保存时发现页面能显示头像但列表页加载不出别人的头像就是因为把临时路径存库了。7.2 图片上传的并发控制与压缩发布页最多9张图每张原图2到5MB如果不做压缩用户流量和云存储费用都遭殃。wx.chooseMedia里的sizeType: [compressed]会帮你压一轮但压缩后的图其实还有好几MB。如果要压得更狠可以再用wx.compressImage把质量压到80%或者用Canvas把最长边压到1280px。上传阶段不要一口气丢9个请求。我在项目里试过Promise.all并发4张图就开始偶发“uploadFile:fail”换成for循环串行后稳定多了。如果你担心用户等待太久可以在UI上显示“已上传3/9”和进度条而不是让用户看着一个菊花转到底。另外上传失败后不要盲目自动重试弱网环境下重试只会堆积更多失败请求提示用户手动操作反而体验更好。7.3 列表页setData数据量过大怎么处理setData是小程序性能的关键。首页分页加载时很多人直接把数据库查出来的完整对象放到data.list里包括content全文和images数组这些字段在列表页根本用不到还会导致每次滚动加载越来越卡。优化策略有几个云函数里用field只返回列表页需要的字段pageSize控制在10条左右用wx:for时给key设置_id图片加lazy-load属性。如果列表特别长社区有miniprogram-list-builder这种原生长列表组件但初版没必要上分页做好就够顺畅了。7.4 地图选点的定位精度问题wx.chooseLocation在开发者工具里很好用但放到真机上经常没反应。不要怀疑代码先检查app.json{ permission: { scope.userLocation: { desc: 用于获取附近美食位置 } }, requiredPrivateInfos: [getLocation, chooseLocation] }少了requiredPrivateInfos真机调用接口会直接失败。同时需要在微信公众平台「开发管理-接口设置」里申请地理位置接口权限个人主体也能申请但要填写使用场景。定位授权弹窗被用户拒绝后要做兜底默认显示一个城市用户依然能浏览内容只有发布和距离排序才需要真实定位。别在授权失败时把整个页面卡死那是体验事故。7.5 真机调试与开发者工具差异开发者工具里跑通的逻辑真机上不一定跑通。最常见的是云函数环境问题工具里你选了一个默认环境但真机上所有云函数都请求到了正式环境报错environment not found。统一做法是在云函数初始化时用cloud.DYNAMIC_CURRENT_ENV不需要在代码里写死环境ID。真机上没有console.log输出调试时可以用vConsole也可以打开右上角菜单的“调试”按钮或者在小程序代码里埋一个调试模式开关。基础库版本也很重要老基础库不支持新API新基础库可能丢旧接口开发时建议最低基础库统一设为2.20.0以上。8. 发布上线与后续迭代从毕设到能用的产品8.1 体验版分发与试用反馈收集很多任务书都要求“能运行”但只给导师看开发者工具里的效果说服力不够。正确做法是发布体验版让同学和导师在手机上真机试用。流程很简单开发者工具右上角“上传”填版本号和备注登录微信公众平台在「版本管理-开发版本」里找到刚上传的版本点“选为体验版”然后在「成员管理-体验成员」里添加试用者的微信号把体验二维码发给他们。这里注意体验版二维码不是谁扫都能用没有加为体验成员的用户扫码会提示无权限。体验版的最大价值就是收集反馈我建议在个人中心放一个“意见反馈”入口或者拉一个群让试用者直接语音描述问题比你每天追着问效率高很多。8.2 审核注意事项如果是正式发布提审前一定要检查三件事第一是类目。地方美食分享需要选择美食或生活服务类目内容涉及用户发布需要补充UGC社区规范后台要填“用户协议”和“隐私保护指引”。第二是隐私接口声明位置、相册、麦克风如果以后要录视频都要在隐私保护指引里明确列出。第三是内容安全策略有用户发布的平台必须提供审核和举报入口否则审核员大概率拒审。平台审核的体验路径是连续操作登录、浏览、发布、删除。建议你在提审时附上测试账号说明告诉审核员每个功能怎么走减少不必要来回。企业主体的小程序每年还要做年审个人主体通常不需要但一定要确认你的账号主体认证状态没过期否则线上版本可能被下架。8.3 内容安全与违规图片过滤UGC项目躲不开内容安全。最简单的做法是在createFood云函数里先校验文本再校验图片。文本检测可以调用cloud.openapi.security.msgSecCheck图片检测可以用cloud.downloadFile把云存储图片读出来再调用cloud.openapi.security.imgSecCheck检测不通过就拒绝入库。实际开发里图片检测接口有频率限制所以我不建议对9张图全部检测至少检测封面图用户上传的每一张图片最好也在前端调用一次检测失败就提示用户更换。只做文本不做图片上线后很容易被举报。审核老师抽查时也会问“你如何防止有人发垃圾广告”把这段逻辑讲清楚是稳定加分项。8.4 迭代方向从分享到社区第一版的核心目标是把内容闭环跑通。如果任务书后续还要扩展或你想把它做成一个真正的产品我认为最容易出效果的方向有三个一是做关注关系用户可以关注感兴趣的探店博主形成信息流分发二是做“吃货等级”和签到提高留存三是做商家合作发布优惠券和套餐但这涉及商业合规需要仔细评估。技术上云开发的聚合能力也足够支撑排行榜、热门分类、用户画像这些功能。但迭代不是无脑加功能而是先看数据用户到底在哪个页面停留最久收藏最多的是哪类美食搜索词集中在哪些城市这些数据用订阅消息或数据分析工具埋点都能拿到。把第一版的使用反馈收集完整再决定下一步做哪个功能比闭门造车靠谱得多。最后再分享一个我在实际带项目时的小习惯每把任务书里的一个功能模块跑通就用手机录一段15秒的演示视频放在答辩PPT里。小程序是强交互产品页面截图看不出数据流15秒视频能让评审老师一眼看懂你是真做了还是只画了界面。这一条对任何“基于微信小程序”的题目都适用地方美食分享也不例外。
返回列表