资讯详情

资讯详情

微信小程序日程管理开发实战:提醒、分享与支付闭环

做日程管理小程序这个选题并不是一时兴起。我自己就是个重度“时间安排困难户”手机自带日历只能记不能提醒得恰到好处市面上的番茄钟、待办类工具要么太重要么订阅消息配合稀烂。2023年年初我花了三周做了一套个人日程时间计划分享提醒管理系统微信小程序把日程创建、时间计划、提醒触达、日程分享全部塞进同一个工具里。这个项目最大的价值不在于代码量而在于业务链路很完整用户登录、数据存储、订阅消息、分享裂变、支付商业化全都能跑通。文章适合三类人看准备做工具型小程序的独立开发者、拿小程序做毕业设计的在校生以及已上线小程序但订阅消息和分享模块一直没理顺的同行。每一步我都尽量讲清背后的“为什么”顺便把我真机调试和审核阶段踩的坑也列出来。1. 项目定位与技术选型为什么我做的是“个人”日程管理1.1 核心痛点轻量日程工具的三大需求市面上能用的日程管理工具并不少但我调研了一圈发现针对中文用户“个人轻量管理临时性分享”这个场景几乎没有称手的选项。大厂日历应用功能齐全但协同办公属性太强对一个普通用户来说满屏的日程组、共享日历、会议室预约反而成了负担。而小而美的待办工具通常只解决了“记下来”这一步到了“到点提醒”和“分享给朋友”这两个关键动作上支持得并不好。所以这个项目的核心需求非常明确第一创建日程要快两步之内完成一条日程录入第二提醒要能按时按量触达系统级推送不可靠就用微信订阅消息来做第三日程要能分享用户可以把自己的时间计划发给微信好友对方不用安装额外应用就能看到。前两点需求大家都能想到第三点才是这个项目的差异化所在也是我后续在架构上单独为“分享”做了数据隔离设计的原因。1.2 功能模块怎么切分才不会失控一个完整的日程管理系统如果按业务域切分可以分成四块日程管理域、提醒触达域、社交分享域、用户与商业化域。我第一版只做了日程管理域和提醒触达域上线后收到用户反馈“能不能把这个日程发给对象/同事”才补上了社交分享域。这个顺序很重要MVP阶段不要把四块都做完先跑通核心闭环再迭代。日程管理域包含日程的增删改查、分类标签、重复规则每天、每周、每月以及日/周/月三种视图。提醒触达域是技术复杂度最高的部分涉及微信订阅消息的授权、配额管理、定时触发器。社交分享域解决的是单个日程通过卡片分享、参数携带、接收方鉴权的问题。用户与商业化域则包括登录态管理和微信支付v3对接以及会员权益配置。四块业务相互独立数据模型上通过用户openid和日程id做关联模块之间耦合度控制在最低水平。1.3 原生小程序还是uni-app我为什么选原生技术选型是项目第一步就要拍板的事情。日程管理类小程序没有复杂的跨端需求主要是iOS和Android两端不涉及App打包也没有桌面端诉求所以我直接选了原生微信小程序开发没有用uni-app或Taro。原生方案最直接的收益是调试最顺畅微信开发者工具里能直接看Network请求、Storage存储订阅消息、支付、分享这些微信特有的能力都能直接用不用等框架适配。后端我选了微信云开发而不是自建SpringBoot服务。做这个选择主要考虑了两点一是单人项目服务器成本要压到最低云开发自带免费额度对个人项目完全够用二是日程数据的安全边界比较清楚云函数里写数据库操作天然避开了前端暴露数据库密钥的问题。当然如果项目后续要接入复杂的外部系统比如企业内部SSO云开发可能就不够了但现阶段绰绰有余。2. 数据模型与日程核心逻辑设计2.1 四张核心表覆盖全部业务场景数据模型是整个系统的地基我设计了四张核心集合users、schedules、tags、schedule_shares。用户表存openid、昵称、头像、订阅消息的剩余配额日程表存创建者、标题、描述、开始/结束时间、是否全天、重复规则、标签ID标签表存名称和颜色分享关系表存日程ID、分享来源用户、接收用户、访问权限。这里有个关键设计日程表里只存创建者openid不冗余“分享给谁”这个字段。分享关系单独建表原因是一个日程可以被分享给多个人如果用数组字段存放后续要查“某人收到了哪些日程”就得全表扫描。建独立分享关系表之后按接收者openid查询走索引一次DB查询就能拿到该用户可见的全部日程id列表。日程量级上来以后这个差别非常明显。// schedules集合核心字段 { _id: schedule_id, _openid: creator_openid, title: 每周例会, description: 周会同步进度, startTime: 1735689600000, // 毫秒时间戳 endTime: 1735691400000, isAllDay: false, repeatRule: weekly, // none | daily | weekly | monthly tagId: tag_xxx, remindTimes: [5, 30], // 提前5分钟和30分钟提醒 createTime: 1735603200000 }2.2 时间戳、时区与提醒时间的计算时间处理是日程应用最容易出bug的地方。我踩过一个典型的坑用户在手机上手写输入“明天早上9点”的日程前端通过new Date(2024-01-15 09:00:00).getTime()转成时间戳存到数据库。这个操作本身没问题但如果后端云函数部署在非东八区时区的服务器上或者用户手机时区设置异常同样的日期字符串转出来的时间戳可能就差了好几个小时提醒就会早发或晚发。稳妥的做法是全链路统一使用毫秒时间戳存储前端在做日期格式化时才转成用户本地时区的字符串。云函数里的定时触发器去扫“需要提醒的日程”时一律用Date.now()和存好的时间戳做比较不做任何时区转换。至于提醒时间如何计算我建议用“偏移量数组”而不是单字段remindTimes: [5, 30]表示在日程开始前5分钟和30分钟各提醒一次。以后要支持自定义提醒间隔前端改一下选择器就行后端不用动。2.3 本地缓存与云端增量同步怎么配合日程管理是一种高频读、低并发、数据量小的应用场景。每次启动小程序都全量拉取所有日程网络慢时体验非常差所以我做了两级缓存本地缓存Storage里存最近30天到未来90天的日程列表和最后同步时间戳每次进入首页时先渲染缓存再向后端发起增量同步请求。增量同步接口的做法是前端把本地最新一条日程的updateTime传给云函数云函数查询所有updateTime 传入时间的记录返回。第一次登录时前端没有本地缓存值传0即可云函数会返回全量数据。这样做的直接收益是用户打开小程序基本不用等loading日程数据秒出同时每天只有少量新增/修改的记录走网络流量开销几乎可以忽略。3. 提醒功能实现从订阅消息到完整触达链路3.1 订阅消息的配额机制必须先明白微信订阅消息和传统的模板消息最大的不同在于“授权即消费”机制用户每次通过wx.requestSubscribeMessage授权你只能给他发一次模板消息发完下次还要再要授权。这个限制直接决定了日程提醒的功能设计。我在做需求时本来想做成“用户创建日程时开启提醒之后每个日程到点都能收到消息”但微信一次性订阅消息的机制不支持这种无限触达。权衡之后我的方案是创建日程时引导用户为这条日程单独授权一次一条日程对应一次提醒授权。对于高频使用的用户在设置中心提供一个“批量授权”按钮一次性请求多个模板ID的授权把额度囤下来。实测下来用户对这个逻辑是能理解的但要在UI文案上讲清楚否则容易被误认为产品bug。3.2 前端授权与云函数发送的完整代码订阅消息的实现分两步前端授权、云函数发送。前端在用户创建或编辑日程后调用wx.requestSubscribeMessage请求订阅授权。// 前端请求订阅授权 function requestRemindPermission(templateIds) { return new Promise((resolve, reject) { wx.requestSubscribeMessage({ tmplIds: templateIds, success(res) { // res[templateId] 可能为 accept、reject、ban const accepted templateIds.filter(id res[id] accept) resolve(accepted.length 0) }, fail(err) { reject(err) } }) }) }reject表示用户这次拒绝了授权但下次还能再请求ban表示用户勾选了“总是保持以上选择不再询问”这种情况后续不能继续弹窗请求只能引导用户去设置页手动打开订阅消息权限。这里有个容易忽略的点调wx.requestSubscribeMessage必须由用户主动点击行为触发在异步回调和setTimeout里调用都会直接失败。用户授权之后真正的发送动作在云函数里完成云端使用cloud.openapi.subscribeMessage.send接口下发消息。// 云函数发送订阅消息 const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main async (event) { const { openid, templateId, page, data } event try { const result await cloud.openapi.subscribeMessage.send({ touser: openid, templateId, page, data, miniprogramState: formal // 发布后使用 formal }) return { code: 0, data: result } } catch (err) { // 常见错误43101用户拒收、47003参数不匹配 return { code: err.errCode || -1, errMsg: err.errMsg } } }这里data字段的格式非常严格必须和你在小程序后台申请模板时的字段名完全一致。腾讯官方审核模板消息时会展示字段占位符比如{{thing1.DATA}}、{{time2.DATA}}云函数里传的key也必须是thing1、time2。我第一次对接时少传了一个字段结果微信一直返回47003参数错误排查了很久才发现是模板字段值超长thing类字段限制20个字符以内中文按字数截断。3.3 定时触发器让提醒在正确的时间点发出有了授权和发送接口剩下最关键的问题是“谁来决定什么时候发”。我的做法是使用云函数定时触发器每60秒执行一次扫描找出所有当前时间满足提醒条件的日程并下发消息。云函数定时触发器在config.json里配置{ triggers: [ { name: scheduleRemindTrigger, type: timer, config: 0 * * * * * * } ] }0 * * * * * *表示每分钟的第0秒触发一次注意这是七段式cron表达式和常见的Linux cron五段式不一样第一次配置时很容易写错。云函数内部写一个checkAndSend逻辑// 云函数入口 exports.main async () { const now Date.now() const db cloud.database() const _ db.command // 查找“触发时间在最近60秒内”的日程 const { data: schedules } await db.collection(schedules) .where({ remindAt: _.and(_.gte(now - 60000), _.lte(now)) }) .limit(100) .get() // 遍历日程检查是否有未发送的提醒记录防重 ... }防重复发送是个很容易被忽略的问题。定时触发器每次执行间隔60秒如果某次执行因网络超时导致发送接口实际上成功了但云函数收到错误码返回失败下一次执行又会再发一遍用户就可能收到两条重复提醒。我的解决方案是发送前先向remind_logs集合插入一条记录以scheduleId remindTime作为唯一键发送前检查是否已存在存在就跳过。3.4 订阅次数不够时的兜底方案订阅消息的配额天然受限用户不可能无限授权所以提醒系统一定要有兜底方案。我在产品里设计了两个兜底第一个是站内“消息中心”日程到期推送逻辑中除了调订阅消息接口还会向消息中心写入一条记录用户打开小程序后能在“提醒”tab里看到所有历史提醒。第二个是首页红点提醒通过云函数把需要提醒的日程标记为hasPendingRemind: true用户打开小程序时首页日程条目标记红点并配合短振动提升感知。不过坦率讲这两个兜底方案都依赖用户主动打开小程序触达能力远不如订阅消息。如果你后续能拿到公众号模板消息的权限把小程序和公众号绑定还可以在用户关注公众号的条件下通过公众号消息再次触达但受限于主体资质和用户关注情况这个只能算锦上添花不能作为主力方案。4. 日程分享与社交能力实现4.1 分享卡片参数传递的一整套方案日程分享是本项目里最有社交裂变潜力的功能。用户把某条日程分享给好友好友点开卡片直接看到日程详情如果觉得有用还能“收藏到我的日程”。实现这个功能的核心在于分享卡片的参数传递。小程序分享通过onShareAppMessage定义返回的path字段可以携带自定义参数Page({ onShareAppMessage() { const schedule this.data.currentSchedule return { title: 邀请你查看日程${schedule.title}, path: /pages/detail/index?id${schedule._id}from${this.data.userInfo.openid} } }, onShareTimeline() { const schedule this.data.currentSchedule return { title: 我的日程${schedule.title}, query: id${schedule._id} // 注意朋友圈分享不支持 from 参数 } } })这里有个细节值得提微信开发者工具里模拟器打开分享卡片时path里的参数是可以正常获取的但朋友圈分享onShareTimeline的query参数不能太长项目早期我把整个日程对象都序列化穿进去结果被微信静默截断了。后来统一改成只传id详情页根据id从云端拉数据。4.2 被分享者进入后的鉴权与数据隔离被分享者点开卡片最先落到详情页onLoad接收参数。此时要判断用户是否已经登录小程序如果未登录则跳转登录页登录完成后带着scheduleId和from参数跳回详情页。数据隔离是分享场景下必须考虑的安全问题。我的做法是云函数里加了一个getSharedSchedule接口前端不能直接通过db.collection(schedules).doc(id).get()读取必须走云函数。云函数内部先判断当前调用者openid是否为日程创建者如果是再查schedule_shares表确认调用者在分享关系里两者都不满足就返回无权限错误。// 云函数获取他人分享的日程详情 exports.main async (event) { const { OPENID } cloud.getWXContext() const { scheduleId } event const scheduleRes await db.collection(schedules).doc(scheduleId).get() const schedule scheduleRes.data if (schedule._openid OPENID) { return { code: 0, data: schedule } } // 非创建者校验是否在分享关系里 const shareRes await db.collection(schedule_shares).where({ scheduleId, toUserOpenid: OPENID }).count() if (shareRes.total 0) { return { code: 0, data: schedule } } return { code: -1, msg: 无权访问该日程 } }4.3 外部链接唤起小程序的几种姿势除了微信内部的分享卡片日程管理类工具往往还有“从外部触达”的需求比如短信提醒用户查看日程、运营活动页跳转小程序。微信为此开放了URL Scheme和URL Link两类能力通过云端cloud.openapi.urlscheme.generate可以生成一个weixin://开头的scheme链接放到短信或邮件里用户在微信中打开就能直接跳进小程序指定页面。实际操作中要注意两点URL Scheme有1万个/月的生成上限个人开发者的免费额度下不要每条日程都生成长期Scheme场景有限的话建议用付费方案或限制频率URL Link则适合分享到App外部网页的场景。早期我为了方便测试直接把weixin://dl/business这种协议硬编码到网页里后来发现这类scheme在部分安卓机型上会直接提示“无法打开”正确的做法是基于官方文档按固定格式拼装参数不要依赖非公开协议。5. 商业化接入微信支付v3对接与常见违规自救5.1 会员体系怎么设计不用太复杂日程管理小程序现阶段免费用户已经能覆盖80%的日常需求商业化诉求集中在高级功能解锁上。我把付费点切成了两类一类是高级提醒次数免费用户每天只能创建3条带订阅消息提醒的日程会员用户不限量另一类是高级视图比如“年视图”“日历导出”这种对日程应用不算核心但对效率型用户有吸引力的能力。会员体系的设计原则是尽量少地侵入正常流程。我的做法是用户完成支付后在users表里写入vipExpireTime字段前端通过云函数返回该字段控制页面上的功能开关。这里要提醒一句不要在前端写死会员判断逻辑前端所有对云端的敏感操作都必须在云函数里二次校验会员状态否则支付接口被绕过就能白嫖高级功能。5.2 支付v3接入的核心步骤后端做支付我推荐直接对接微信支付v3而不是老版v2。v3用RSA非对称签名配合APIv3密钥做AES-256-GCM解密安全性和易用性都比v2好很多。整体流程是前端调wx.requestPayment发起支付云函数先调微信支付统一下单接口拿到prepay_id然后拼接支付参数返回给前端。回调部分需要配置一个HTTPS接口接收微信支付结果通知通知里的关键数据用APIv3密钥解密拿到订单状态。第一次对接时最容易卡在证书问题上。v3要求在商户平台“API安全”里设置APIv3密钥并且下载商户证书用于请求签名。常见报错是“无可用的平台证书”原因是v3的证书体系里有一个平台证书用于验签微信回调的响应内容不是商户自己的API证书。要先用商户私钥调一次“获取平台证书”接口把平台证书下载并缓存下来后续用这个平台证书验签回调。如果你用的开发框架我在云函数里用的wechatpay-node-v3已经内置了平台证书自动更新逻辑这个坑就能绕过去。5.3 “支付功能暂时无法使用”的排查清单接入支付后最让人头疼的是小程序突然被提示“由于小程序违规支付功能暂时无法使用”。这个提示通常出现在小程序后台“功能-微信支付”模块原因是触发了平台风控或违规审查不一定是支付代码的问题。我从自己踩坑和同行交流中整理了一份排查清单第一步先去小程序后台“申诉与反馈”查看是否有违规记录优先处理违规内容再申诉第二步检查类目是否齐全个人主体的小程序有很多类目不支持开通支付必须转成企业主体第三步检查用户隐私保护指引尤其是收集用户头像昵称等内容必须在小程序后台“用户隐私保护指引”中声明第四步检查有没有诱导分享、虚拟支付等违规场景比如“分享后解锁高级功能”这类设计很容易被判定为诱导分享。如果真的被停了支付功能整个过程没有捷径只能按申诉流程提交资料。这里给一个实操建议开始做支付功能前就先把隐私协议、类目资质、内容安全能力全部配好免得项目上线后因为小问题被连坐封了支付连累整个小程序无法使用。6. 踩坑实录与高频问题解决速查6.1 订阅消息收不到八成是这两个原因我上线后收到最多的用户反馈是“我创建了日程也点了提醒为什么到时间没收到消息”。排查后八成是两类问题一是模板ID不匹配开发者工具测试版和正式版小程序用的模板ID必须各自申请我见过有人把体验版的模板ID发到正式环境结果所有消息都报43101二是跳转页面路径错误订阅消息里的page参数要求是已发布页面路径填错了微信不会报错但消息点击后只会打开小程序首页用户感知不到提醒内容。另一个隐蔽问题是云函数的定时触发器没有部署。云函数本地调试时一切正常但定时触发器必须在云开发控制台部署成功后才会生效。部署后还要注意触发器使用默认的“测试环境”还是“正式环境”如果环境不一致触发时查不到数据等于白跑。6.2 自定义导航栏高度计算一个函数搞定原生微信小程序页面的标题栏支持自定义使用自定义导航栏时最麻烦的是顶部的胶囊按钮位置在不同机型上不一样如果适配不当布局就会错位。我一开始每个页面都手写死top: 44px后来换成自动计算方案// 获取导航栏和胶囊按钮布局信息 const { statusBarHeight } wx.getWindowInfo() const menuRect wx.getMenuButtonBoundingClientRect() const navBarHeight (menuRect.top - statusBarHeight) * 2 menuRect.heightstatusBarHeight是状态栏高度menuRect是右上角胶囊按钮的left、top、width、height。导航栏高度等于(胶囊上边缘到状态栏底部的距离)*2 胶囊高度这个公式在几乎所有机型上都适用可以保证自定义导航栏和胶囊按钮垂直对齐。注意wx.getWindowInfo()是较新的API老版本项目里还在用wx.getSystemInfoSync()后者已经从基础库2.20.1开始废弃。6.3 uniapp开发时软键盘遮挡输入框的解决思路虽然我推荐原生开发但后台陆陆续续有同行问用uni-app写小程序时“手机软键盘会遮挡查询内容”怎么处理。这个问题主要出在输入框位于页面底部时软键盘弹出后安全区高度变化页面底部View被键盘覆盖。常见思路是在onKeyboardHeightChange事件里监听键盘高度动态给底部容器设置marginBottom。uni.onKeyboardHeightChange在微信小程序端可用但在App端不支持。不过做微信小程序的前提下可以这样处理uni.onKeyboardHeightChange(res { this.keyboardHeight res.height this.$nextTick(() { uni.pageScrollTo({ scrollTop: this.swiperHeight, // 滚动到底部 duration: 300 }) }) })这个方案能解决大部分遮挡问题但要注意键盘弹起时页面会自带一次滚动如果再加pageScrollTo可能出现抖动。更好的做法是改用adjust-position属性默认为true当键盘弹起时微信会自动把页面上推不需要手动处理。手动处理往往是在adjust-position有bug时才需要。6.4 发布审核被驳回的真实案例小程序审核拦路虎主要集中在隐私合规和类目选择上。我的日程小程序第一次提审就被驳回原因是“涉及用户个人信息收集未提供隐私协议”。当时的隐私协议只是简单写在关于页里但微信后台要求在小程序管理后台“设置-服务内容声明-用户隐私保护指引”中逐项声明收集的信息类型并且前端在收集用户头像昵称前必须弹窗明确告知。第二次被驳回是因为“诱导分享”嫌疑详情页有一个“分享给好友解锁高级模板”的按钮。平台认为这是诱导分享。我改成了“分享后可查看”也是不行到最后直接把分享和权益解耦分享按钮只承担分享功能不再和任何权益绑定审核才通过。这个案例的教训是小程序生态对“利益诱导分享”管得很严产品设计阶段就要避开。6.5 接口调试抓包和源码保护的一点经验开发阶段调试接口我优先使用微信开发者工具自带的Network面板可以看到所有请求的URL、请求头、响应体信息足够详细。真机上想抓包我试过在PC上配置代理配合抓包工具但要先处理小程序的证书校验操作繁琐且不一定稳定实际收益也不大。日常开发建议直接依赖开发者工具的调试能力能省下不少排查时间。源码保护是个被讨论了很多的话题。小程序的代码是分包上传的前端包理论上可以被反编译还原所以不要在前端代码里放任何核心算法或密钥参数。我项目的所有敏感逻辑全放在云函数侧前端拿到的永远只是展示数据。即使前端被反编译攻击者也拿不到数据库访问凭证这块算我在安全设计上比较放心的地方。6.6 网络异常时的全局统一提示移动端网络环境不可控日程应用又强依赖数据同步网络异常处理不好用户会以为数据丢了。我实现了一个全局监听器在app.js里监听wx.onNetworkStatusChange检测到断网时全局弹出轻提示恢复网络后自动重新拉取数据。// app.js 全局网络监听 App({ onLaunch() { wx.onNetworkStatusChange(res { if (!res.isConnected) { wx.showToast({ title: 网络已断开, icon: none }) this.globalData.isNetworkConnected false } else { this.globalData.isNetworkConnected true // 触发全局数据刷新 this.refreshAllData() } }) } })这个小功能的体验提升非常明显尤其是用户在地铁、电梯这些信号不稳定的场景不会因为一次拉取失败就误以为日程丢失。6.7 发布流程与版本管理小程序的发布流程比较固定代码上传后先走体验版确认没问题在后台提交审核审核通过后点“发布”。但版本管理容易出岔子。微信开发者工具上传代码时每一次上传都会对应一个“开发版本”同一个版本只能有一个体验二维码新上传的版本会把旧的顶掉。如果你的测试同事还在用旧二维码体验版更新后他手里的二维码可能就失效了需要重新扫。我建议给每个里程碑版本打tag比如v1.0.0、v1.1.0并在微信开发者工具上传备注里写清楚改动点这样提交审核时比较清晰。对应的云端数据库结构如果有变更也要同步做好线上和体验环境的数据迁移脚本。我第一次做数据库结构变更时没写迁移脚本导致体验版和正式版数据格式不一致界面直接白屏后来老老实实补了兼容代码才算稳定。小程序做个人日程管理这个方向最大的门槛不在代码量而在于把微信的平台能力组合好。日程创建、订阅消息授权、定时触发、分享卡片、支付回调每一块都单独看文档不算难但串起来做对一个完整的业务闭环就会出现各种边界问题。我在这个项目里花时间最多的其实就是测试各种异常场景用户不授权、授权后取消、定时任务重复执行、网络断开恢复、支付回调延迟……每一类异常都有对应的处理逻辑最终才敢把系统放出去让人用。如果你准备做类似的小程序我个人的建议是先把订阅消息跑通再把分享闭环跑通最后再加支付顺序不要乱。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →