ARTICLE DETAIL

资讯详情

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

云开发情侣小程序实战:任务积分兑换闭环一次讲透

云开发情侣小程序实战:任务积分兑换闭环一次讲透 简介面向情侣互动场景的微信小程序云开发项目包适合想用小程序记录二人日常、通过任务与积分机制提升互动趣味性的开发者或情侣用户。项目完整运用云开发数据库、文件存储与云函数三大基础能力前端可直接读写文档型数据库、上传下载云端文件云函数实现鉴权与业务逻辑。压缩包内共一百六十三个文件含三十八个JSON配置、三十四个JS逻辑、三十一个WXSS样式、二十九个WXML页面模板另有图片素材与说明文档整体大小约一点七二兆字节目录结构清晰便于按功能模块定位。目前已有千二百三十三人学习下载。通过这份项目读者可掌握云开发控制台环境配置、环境ID替换等部署细节并获得一套可直接运行的小程序源码既能用来布置专属情侣空间也能作为微信小程序与云开发结合的实操范例。1. 云开发情侣互动小程序做任务、攒积分、换商品闭环的一次完整落地情侣应用在小程序里一直不缺热度但大多数demo做完留言板、打卡日历就停了真正能跑通“做任务、攒积分、换商品”这个产品闭环的很少。这个标题给的是一份可直接落地的微信小程序示例两人绑定情侣关系一方发布任务另一方完成后获得积分积分在小店里兑换实物或虚拟福利。它把云开发后端、微信小程序前端、双人互动逻辑串在了一起适合两类人——想学云开发的开发者可以照着它理解“前端只做交互、云函数管业务规则”的写法想快速做情侣向小产品的人则可以直接拿它当起点省掉从零设计数据模型和业务流程的功夫。这篇笔记按从环境初始化到上线自测的顺序拆每个关键点都给代码和参数。2. 从零初始化云开发环境四个集合搭起情侣关系与任务商品模型2.1 导入项目与开通云开发先确认基础库版本拿到这份源码压缩包后第一步不是在代码里找功能入口而是先把微信开发者工具的项目导入路径指到解压后的目录。导入时开发者工具会要求填AppID建议用你自己的测试号或已认证的小程序AppID因为云开发环境、订阅消息、真机预览都依赖一个真实的小程序账号。如果直接用“测试号”云开发控制台的部分能力会受限后面排查问题会多绕弯路。导入后再看app.js这是云开发的入口。确认基础库版本在 2.2.3 以上云开发 API 才能正常工作。打开app.js你会看到类似下面的初始化逻辑App({ onLaunch() { if (!wx.cloud) { console.error(请使用 2.2.3 或以上基础库以使用云能力); return; } wx.cloud.init({ env: couple-app-xxxx, // 替换成你自己的云开发环境ID traceUser: true }); } });这段代码里env字段填的是云开发环境ID在开发者工具工具栏“云开发”入口创建环境后可以看到形如couple-app-1a2b3c。traceUser: true的作用是让云开发在用户调用云函数时自动记录访问来源方便你在控制台查看调用日志。不要小看这个开关排查“某个用户操作失败但前端没报错”时云开发控制台的日志面板是核心线索。2.2 集合设计couple、tasks、products、orders 与 score_logs云开发的数据模型是文档型数据库每个集合里存的是一篇篇 JSON 文档。情侣互动小程序的核心集合有五个我在做类似项目时习惯先画一张字段表再动手写代码。集合名关键字段用途说明couplecoupleId、memberOpenids、scoreMap、status记录一对情侣的关系scoreMap 存双方各自积分taskstaskId、coupleId、creatorOpenid、assigneeOpenid、title、points、status任务主表status 区分 pending / ongoing / doneproductsproductId、name、points、stock、type、image兑换商品池type 区分实物和虚拟福利ordersorderId、coupleId、openid、productId、pointsSpent、status兑换订单记录谁用多少积分换了什么score_logslogId、coupleId、openid、delta、type、orderId、time积分流水type 区分任务奖励和兑换扣减其中 couple 集合是整个系统的枢纽。我常用的做法是每对情侣一条文档memberOpenids是长度为 2 的数组scoreMap是一个对象形如{ oXXXX: 120, oYYYY: 80 }。把积分放在 couple 文档里比放在用户各自文档里更直观查询一次就能拿到双方积分做对比展示。2.3 用初始化云函数自动建表应急建表真的省事很多新手会在云开发控制台手动点“创建集合”每次开发环境切换都要重建一遍非常麻烦。我一般会写一个一次性的初始化云函数来建表放到云函数目录里部署执行一次即可。// cloudfunctions/initDb/index.js const cloud require(wx-server-sdk); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db cloud.database(); exports.main async () { const collections [couple, tasks, products, orders, score_logs]; const results []; for (const name of collections) { try { await db.createCollection(name); results.push({ name, created: true }); } catch (err) { // 集合已存在时云开发返回 -5010000 if (err.errCode -5010000) { results.push({ name, created: false, reason: already exists }); } else { results.push({ name, created: false, reason: err.errMsg }); } } } return results; };这个云函数的逻辑很简单循环尝试创建集合遇到“已存在”错误码就跳过最后返回每个集合的创建结果。部署后在开发者工具的“云函数”面板里点“云端测试”传入空事件{}即可。云开发免费版的集合数量有限制五个集合完全在配额内但要注意集合名一旦创建无法改名只能删了重建所以命名一开始就定好。把建表脚本放进项目里还有一个好处换新环境、给客户交付时部署一次云函数就能恢复全部表结构不用对着控制台一遍遍点鼠标。3. 做任务攒积分云函数管结算逻辑前端只做交互3.1 任务发布与接单参数校验放在服务端情侣任务的创建流程是发起者在页面里填任务标题、选择积分值、指定由对方还是自己完成然后点发布。这部分前端代码只需要采集表单数据并调用云函数真正的业务规则放在云函数里。原因很简单小程序端代码可以被反编译或通过抓包工具模拟请求如果把积分值、发布人这些字段完全信任前端对方把积分改成 9999 就能刷爆整个系统。// cloudfunctions/createTask/index.js const cloud require(wx-server-sdk); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db cloud.database(); const _ db.command; exports.main async (event) { const { OPENID } cloud.getWXContext(); const { title, points, assigneeOpenid, coupleId } event; if (!title || !points || points 0 || !assigneeOpenid) { return { code: PARAM_ERROR, msg: 参数不完整 }; } const coupleDoc await db.collection(couple).doc(coupleId).get(); if (!coupleDoc.data.memberOpenids.includes(OPENID)) { return { code: NOT_COUPLE, msg: 只有情侣成员才能发布任务 }; } await db.collection(tasks).add({ data: { coupleId, creatorOpenid: OPENID, assigneeOpenid, title, points, status: ongoing, createdAt: db.serverDate() } }); return { code: OK }; };cloud.getWXContext()获取的OPENID是用户身份的唯一标识服务端用它来判断“这个人是不是情侣文档里的成员”。这里的核心逻辑是两步第一步校验参数和权限第二步写入任务。不要为了省一次数据库查询而省略权限校验——你没法保证调用这个云函数的请求一定来自小程序页面它可能来自任何人的命令行。积分值建议限定在 1 到 100 的区间防止有人把任务积分设成负数钻空子。3.2 积分结算事务加 inc先锁定再写入任务完成后的结算是最容易出错的地方。常见错误写法是前端先查当前积分在本地加上任务积分再 update 写回。这个流程在两个人同时操作时会互相覆盖。比如女朋友提交任务完成的同时男朋友正在兑换商品扣积分两次读写交叉最终积分就可能算错。正确的做法是使用云数据库的事务配合inc原子操作。下面的云函数是任务结算的核心// cloudfunctions/settleTask/index.js const cloud require(wx-server-sdk); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db cloud.database(); const _ db.command; exports.main async (event) { const { OPENID } cloud.getWXContext(); const { taskId } event; const transaction await db.startTransaction(); try { const taskRes await transaction.collection(tasks).doc(taskId).get(); const task taskRes.data; // 状态机校验只有 ongoing 的任务才能结算 if (task.status ! ongoing) { await transaction.rollback(); return { code: TASK_DONE, msg: 任务已结算过 }; } // 只有被指派人可以完成且必须属于同一情侣关系 if (task.assigneeOpenid ! OPENID) { await transaction.rollback(); return { code: NO_PERMISSION, msg: 无权完成此任务 }; } // 给执行者加积分 await transaction.collection(couple).doc(task.coupleId).update({ data: { [scoreMap.${OPENID}]: _.inc(task.points) } }); // 任务置为完成态 await transaction.collection(tasks).doc(taskId).update({ data: { status: done, doneAt: db.serverDate() } }); // 记录积分流水 await transaction.collection(score_logs).add({ data: { coupleId: task.coupleId, openid: OPENID, delta: task.points, type: task_reward, orderId: , time: db.serverDate() } }); await transaction.commit(); return { code: OK, points: task.points }; } catch (err) { await transaction.rollback(); throw err; } };这个函数里最重要的两个设计一个是事务包裹所有读取和写入另一个是每个操作前先做状态校验。scoreMap.${OPENID}这种动态 key 写法配合_.inc(task.points)可以在一次请求里把积分增加指定数值不需要先查再算。事务保证要么全部成功要么全部回滚不会出现“积分加了但任务状态没更新”这类脏数据。云开发事务有超时限制函数里应避免在事务内做耗时的外部请求。3.3 任务进度实时同步watch 比轮询顺手但要控数量情侣任务列表有个特殊需求——两个人同时在看同一个页面一方点了完成另一方界面要尽快看到状态变化。传统做法是前端 setInterval 每 5 秒拉一次数据体验一般。云开发数据库的 watch 能力可以监听集合数据变化实时推送变更。前端监听任务的代码大致是这样// pages/tasks/tasks.js const db wx.cloud.database(); let taskWatcher null; Page({ onLoad() { const coupleId wx.getStorageSync(coupleId); taskWatcher db.collection(tasks) .where({ coupleId, status: ongoing }) .watch({ onChange(snapshot) { // snapshot.docs 是最新的任务数组 this.setData({ tasks: snapshot.docs }); }, onError(err) { console.error(watch error, err); } }); }, onUnload() { if (taskWatcher) { taskWatcher.close(); } } });watch 的机制是在小程序端建立一条数据监听通道服务端数据变化时主动推送比轮询实时性高很多。但它有两个限制一个页面同时开启的 watch 数量有限超出会报错离开页面时不调用taskWatcher.close()会一直占用通道资源导致后续页面 watch 失效。我的习惯是只在任务列表页用 watch详情页仍然用普通请求查询。另外watch 的where条件要尽量精确把 coupleId 作为过滤条件能明显减少推送的数据量。4. 换商品链路库存扣减与积分流水一次完成4.1 商品数据模型实物与虚拟福利的字段差异商品池的设计决定了“换商品”这个玩法能不能真的落地。实物商品需要库存、快递信息、图片虚拟福利比如“免洗碗一天”“陪看电影一次”没有库存概念但需要说明使用方式。我在 products 集合里会区分type: physical和type: virtual实物有stock数值和image图片地址虚拟商品stock固定为 1多一个description字段说明如何兑现。两者共用points字段标价前端列表页只展示points、name、image详情页再按类型渲染不同字段。上架商品的常见做法是写一个 seed 云函数一次性放入初始商品数据。实际运营当然可以在控制台手动维护但用于演示和测试时云函数初始化更高效// cloudfunctions/seedProducts/index.js const cloud require(wx-server-sdk); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db cloud.database(); exports.main async () { const products [ { name: 情侣奶茶兑换券, points: 30, stock: 10, type: virtual, description: 出示此订单由对方代付奶茶一杯 }, { name: 定制马克杯, points: 80, stock: 5, type: physical, image: cloud://couple-app.636c-something/mug.jpg }, { name: 周末免家务卡, points: 120, stock: 3, type: virtual, description: 使用后当天可不承担家务 } ]; for (const p of products) { await db.collection(products).add({ data: { ...p, createdAt: db.serverDate() } }); } return { code: OK, count: products.length }; };这里的image字段要注意云开发控制台上传的图片拿到的是cloud://开头的文件ID小程序端 image 组件可以直接使用。如果用的是外部图床的 HTTPS 链接需要在微信公众平台配置 downloadFile 合法域名否则真机预览时图片会被拦截。这是新手最容易忽略的一步。4.2 兑换云函数库存、积分、重复下单三次校验兑换商品是扣积分场景和任务结算一样需要事务保护。但兑换比结算多一个库存维度校验顺序很讲究先查库存再查积分最后查重复下单。顺序错乱会出现“扣了积分但没扣库存”或者“库存扣了但积分不足”的中间状态。// cloudfunctions/exchangeOrder/index.js const cloud require(wx-server-sdk); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db cloud.database(); const _ db.command; exports.main async (event) { const { OPENID } cloud.getWXContext(); const { productId, coupleId } event; const transaction await db.startTransaction(); try { // 第一步查商品锁定库存 const productRes await transaction.collection(products).doc(productId).get(); const product productRes.data; if (product.stock 0) { await transaction.rollback(); return { code: SOLD_OUT, msg: 商品已兑完 }; } // 第二步查积分是否足够 const coupleRes await transaction.collection(couple).doc(coupleId).get(); const myScore coupleRes.data.scoreMap[OPENID] || 0; if (myScore product.points) { await transaction.rollback(); return { code: NO_SCORE, msg: 积分不足 }; } // 第三步扣库存、扣积分、写订单、写流水 await transaction.collection(products).doc(productId).update({ data: { stock: _.inc(-1) } }); await transaction.collection(couple).doc(coupleId).update({ data: { [scoreMap.${OPENID}]: _.inc(-product.points) } }); const orderRes await transaction.collection(orders).add({ data: { coupleId, openid: OPENID, productId, productName: product.name, pointsSpent: product.points, status: pending, createdAt: db.serverDate() } }); await transaction.collection(score_logs).add({ data: { coupleId, openid: OPENID, delta: -product.points, type: exchange, orderId: orderRes._id, time: db.serverDate() } }); await transaction.commit(); return { code: OK, orderId: orderRes._id }; } catch (err) { await transaction.rollback(); throw err; } };注意orderRes._id的来源add操作返回的_id是云数据库自动生成的文档 ID写入 score_logs 时把它存进orderId字段方便后续对账时从流水反查订单。三次校验的顺序不能乱因为库存查询是最可能失败的场景先拦截能减少无效事务。真实项目中还要考虑同一个用户短时间重复点击兑换按钮造成重复下单——把status: pending的订单当作未发货处理用户端再次兑换前先查一下有没有 pending 订单。4.3 积分流水每个订单留有快照对账不靠猜积分流水是很多人懒得做的模块但一旦上线出问题它就是你唯一的后悔药。score_logs 集合里我要求每个文档记录delta正负值、type类型和orderId。任务奖励的type是task_reward兑换扣减的是exchange手工调整的可以是admin_adjust。这样前端积分明细页只需要按 coupleId 查询流水列表按时间倒序展示就能还原出每一分的去向。更重要的是对账场景。比如用户反馈积分少了你只需要筛出这个人的 score_logs 做一次汇总和 couple 文档里的 scoreMap 对比就能确定问题是出在漏写流水还是重复扣减。我在运营情侣小程序时把这套流水当作唯一可信数据源scoreMap 只是缓存展示用的冗余字段。如果有一天 scoreMap 坏了用流水逐条重放就能重建全部积分数据这就是流水的价值。5. 云开发情侣小程序避坑实录五个必踩的高频问题5.1 数据库权限默认全拒安全规则要按双人关系设计现象小程序端查询 tasks 列表返回空数组控制台里明明有数据用户提交任务后立即刷新发现刚写入的记录读不回来。原因云开发数据库默认权限是“仅创建者可读写”而情侣互动场景要求双方都能读取对方创建的任务、订单。没有调整安全规则时一方创建的记录对另一方不可见。解决在云开发控制台“数据库-权限设置”里将集合权限改为自定义安全规则。对 couple 集合单独处理因为它涉及积分和关系数据敏感度最高。比如 tasks 集合的安全规则可以写成{ read: doc.coupleId in [auth.openid, get(database.couple. doc.coupleId).memberOpenids[0], get(database.couple. doc.coupleId).memberOpenids[1]], write: doc.creatorOpenid auth.openid }这里get语法是云开发安全规则内置的跨文档查询能力。但我不建议把整个业务逻辑做成安全规则读操作可以用规则放开写操作尽量走云函数。云函数调用数据库是不受安全规则限制的业务校验在服务端做前端只保留只读权限这样权限配置简单也不容易出漏洞。5.2 积分同时扣两次别在前端写积分云函数也要走事务现象两个设备同时操作时积分扣减出现偏差比如兑换一个 50 积分的商品用户积分从 100 变成了 20 而不是 50。原因前端直接调用db.collection(couple).doc(id).update({ data: { score: _.inc(-50) } })确实能原子扣减但整个业务流程缺了事务保护——库存扣了积分扣了订单写入失败部分操作回滚不了。多人并发时更危险积分校验和扣减之间存在时间窗口。解决所有涉及积分变更的路径都必须收敛到云函数并且在函数里使用事务。云函数里事务能覆盖跨集合操作而前端 SDK 做不到。我的底线是“任务结算、兑换下单、积分调整”这三类操作全部走事务云函数前端代码只展示数据不持任何写权限。这样即使有人通过抓包工具伪造请求也只能调用云函数而云函数内部有校验逻辑。5.3 云函数超时与偶发失败3 秒默认值是真凶现象任务结算偶尔报错错误码是 -504002 或提示“function timeout”重试一次又成功了。原因云开发云函数的默认超时时间是 3 秒冷启动时 Node.js 运行时初始化可能要占 1 到 2 秒加上数据库事务操作业务逻辑稍微复杂一点就容易触顶。解决在云开发控制台“云函数-配置”里把涉及事务的云函数超时时间调整为 20 秒。另外可以通过置灰“云函数预加载”或配置固定并发的方式减少冷启动概率。排查这类问题时去云开发控制台的“日志-函数日志”里看耗时记录如果大部分请求耗时在 2.9 秒左右那就是超时临界问题调大超时时间立竿见影。不建议用重试机制掩盖超时——如果函数实际已经执行成功但返回超时重试会造成重复结算必须先用事务和状态机挡住重复操作。5.4 积分为负数、任务重复结算缺少状态机的下场现象同一个任务被结算两次积分加了两次或者兑换时积分被扣成负数用户还能继续下单。原因settleTask 和 exchangeOrder 里都依赖任务状态和积分数值做判断但如果跳过状态校验直接更新或者校验和更新之间没有事务隔离并发请求就会穿透。本质是缺少状态机的约束。解决在数据结构上强制状态流转。tasks 集合的 status 只有三条合法路径ongoing - done已经 done 的任务任何结算请求都直接拒绝。我在上面 settleTask 的代码里已经在事务中先读 status 再判断但还需要给 tasks 表加一个唯一性保障——云开发数据库支持为字段添加唯一索引把 taskId 设为唯一索引能在数据库层面拦住重复的结算记录。至于负数积分兑换前校验myScore product.points直接拦截同时事务里用_.inc(-points)扣减如果某条数据的积分已经损坏唯一索引加约束也兜不住只能靠流水日志人工修复。5.5 订阅消息只能发一次授权要在兑换页面触发现象商品兑换成功想给另一半发一条订阅消息“你的恋人兑换了礼物”结果对方完全收不到控制台报错“45063 或 user refuse”。原因微信小程序的订阅消息采用“用户主动订阅一次平台下发一条”的策略。如果授权弹窗放在首页或设置页用户当时点了允许之后兑换时再想下发授权额度早已消耗。解决把订阅授权请求放到兑换按钮的点击事件里用户在点击“立即兑换”时同时弹出授权框。下面的代码展示了正确时机// pages/shop/shop.js async function onExchange(product) { // 先请求授权再发起兑换 const tmplId your-template-id; const res await wx.requestSubscribeMessage({ tmplIds: [tmplId] }); const granted res[tmplId] accept; const order await wx.cloud.callFunction({ name: exchangeOrder, data: { productId: product._id } }); // 授权额度只在下一次兑换时发给对方 if (granted order.result.code OK) { await wx.cloud.callFunction({ name: sendOrderNotify, data: { templateId: tmplId, orderId: order.result.orderId } }); } }注意这里授权和兑换是两个独立步骤千万不要因为授权失败就中断兑换流程。用户拒绝订阅授权不应该影响他购物只是收不到提醒而已。另外订阅消息模版要在微信公众平台申请并通过审核后才可用真机测试时用测试模板也能下发但用户侧一次授权对应一条消息发完全部额度后继续调 API 会直接报错。6. 上线前自测把体验版发给恋人循环跑三单6.1 真机预览与体验版拿真机而不是模拟器代码写完之后模拟器里的表现不能代表真机。我见过最典型的翻车模拟器里云函数调用一切正常同事用 iPhone 扫码打开体验版页面白屏控制台报invalid env。原因往往是 app.js 里的 env 填的是模拟器默认环境真机用小程序的正式环境ID。另一个常见差异是订阅消息弹窗在模拟器里被跳过真机上才真正走授权流程。把小程序发给对方试用正确路径是在微信开发者工具里点“预览”生成一个带二维码的预览版对方扫码后进入的是开发版如果想让更多人稳定体验点“上传”后在微信公众平台“版本管理”里把代码设为体验版体验版二维码发给测试成员即可。我要强调的是预览和体验版必须用真机跑一遍完整流程尤其是云函数权限、订阅消息授权、相册保存这类涉及系统能力的环节。6.2 一条验收脚本发任务、结算、兑换、对账连跑三次我给自己定过一条死规矩任何涉及积分的小程序上线前至少要在测试环境完整跑通三次“发任务、做任务、结算积分、兑换商品”的链路。下面是我常用的手工验收清单步骤操作预期结果1情侣A发布任务积分值设为20情侣B的任务列表立刻出现该任务2情侣B点击完成任务任务状态变为已完成B的积分20流水新增一条 task_reward3情侣B兑换30积分的虚拟商品兑换成功B的积分-30库存不变4情侣B再次兑换同一商品触发积分不足拦截不会出现负数5控制台查 score_logs与 scoreMap 数值一致可逐条对账跑完这五步这个闭环基本就稳了。最后分享一个我的个人习惯每次改完云函数第一件事不是看功能而是先在控制台跑一次云端测试点开日志确认没有权限错误和超时告警再用体验版真机跑一次全链路。踩过“模拟器正常、真机翻车”“改完函数忘了重新部署、线上还是旧逻辑”这些坑之后你会明白稳定的不是代码是固定的自测习惯。希望这篇笔记能帮你在云开发情侣小程序这条路上少走弯路。提示如果你按步骤操作时遇到某个具体报错优先查云开发控制台的函数日志和数据库权限配置八成的问题都在那里露出马脚。本文还有配套的精品资源点击获取
返回列表