ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

微信小程序打卡签到源码解析:从导入到业务改造全流程

微信小程序打卡签到源码解析:从导入到业务改造全流程

简介:微信小程序开发-易打卡签到案例源码,是一份面向小程序入门与进阶学习者的完整示例项目,覆盖签到功能的页面搭建、业务逻辑、数据交互与状态管理;压缩包共81个文件,约1.51MB,其中包含40张界面图、11个逻辑脚本、10个样式表、9个页面结构、9个配置文件,以及1个演示动图和附带说明文档,结构清晰,便于按模块研读。项目展示了全局应用逻辑、全局配置和公共样式的组织方式,并通过页面目录演示了结构、样式与逻辑分离的开发模式;工具函数和网络请求模块,则体现了用户登录状态管理、签到数据提交与记录获取等真实业务场景。案例还涉及获取用户信息、本地缓存、页面跳转、生命周期函数等小程序核心能力,为开发者提供了可直接复用的签到类应用骨架。已有895人学习浏览,无论新手还是希望快速上手的开发者,都能从中获得清晰的开发思路与排错参考。

1. 先想清楚:为什么“易打卡签到”是小程序开发最值得拆的源码案例

不少前端朋友第一次独立接小程序的活儿,拿到的需求不是商城也不是社区,而是一个内部用的打卡签到。我们团队当时给运营部门做晨会打卡,第一反应就是去找现成的源码。网上“微信小程序开发-易打卡签到案例源码.zip”这类包很多,但直接拿过来编译都过不了的大有人在。这类源码最好也最坑的地方在于,它把微信生态里的核心能力全串了一遍:登录、数据库、日期处理、订阅消息、真机兼容,每个环节都藏着一个能把新手卡住的点。这篇文章就照着这类打卡签到案例源码,把从导入、改规则到排查坑的完整路径讲清楚,适合第一次接触小程序开发、想快速出活又不想反复返工的人。

2. 阅读源码前先立住框架:一个打卡小程序在微信生态里由哪几块组成

拿到源码先别急着点编译。把项目拆开看,它其实就三层:用户怎么被识别、打卡记录存在哪、页面怎么把记录画出来。这三层对应到微信生态,就是 wx.login 换来的 openid 体系、云开发数据库或自建后端、以及小程序页面栈。我习惯先读 app.js 和 app.json,这两个文件能告诉你整个包走的是云开发直连还是后端 API 模式。判断清楚了,后面改代码才不会改到一半发现路径不对。

2.1 从 wx.login 到 openid:用户身份是怎么被认出来的

打卡签到最核心的问题是:一条记录属于谁。小程序没有传统意义上的“账号密码”,微信通过 openid 来标识一个用户。每个用户在每个小程序下的 openid 是唯一的,这就是打卡记录的归属凭证。

前端这一侧,一般会在启动时调用 wx.login 拿一个临时 code,然后把 code 丢给后端或云函数。常见做法是走云函数,省去自己维护服务器的成本:

// 前端拿到 code 后,调用云函数 login 换取 openid wx.login({ success(res) { if (res.code) { wx.cloud.callFunction({ name: 'login', data: { code: res.code } }).then(res => { console.log('当前用户 openid:', res.result.openid) // 把 openid 存到全局,供后续打卡记录写入时使用 }) } } })

云函数那一侧不需要真的拿 code 去换什么,因为云开发环境里可以直接取到上下文:

// cloudfunctions/login/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async () => { const wxContext = cloud.getWXContext() return { openid: wxContext.OPENID, unionid: wxContext.UNIONID, appid: wxContext.APPID } }

这里有几个参数值得注意。wx.login 的 code 是临时凭证,五分钟有效,用一次就作废。如果你走自建后端,需要用 code 加上 appid 和 secret 去调微信的 code2Session 接口,这个 secret 一定不能放在小程序前端代码里,否则等于把钥匙交给了所有人。云开发模式下 getWXContext 直接给 openid,省了这一步,但前提是你得先开通云开发环境。

另外要分清 openid 和 unionid。openid 是当前小程序下的用户唯一标识,unionid 是同一微信开放平台账号下多个应用共用的标识。做打卡签到,记录 openid 就够了,不要没事去依赖 unionid,因为个人主体小程序不一定有开放平台权限。

2.2 日历、日期和时间戳:打卡记录的存储结构怎么设计

看这类源码时,重点看数据库集合里一条打卡记录长什么样。大部分打卡签到源码要么把记录存在一个名为 checkins 或 punch_records 的集合里,要么用一个用户的文档里嵌数组的方式。我建议用独立集合配合索引,而不是嵌套数组,因为后期要查“某天有多少人打了卡”“某人连续打了几天”,独立集合一条记录就是一行数据,统计方便得多。

典型的一条记录长这样:

{ "_id": "自动生成", "_openid": "oX-abc123...", "date": "2026-02-18", "timestamp": 1768512000000, "remark": "晨会打卡", "type": "daily" }

这里有两个字段容易被新手忽略。第一个是 date,我强烈建议用“YYYY-MM-DD”字符串存,而不是用 Date 对象直接入库。原因后面在避坑章节会细说,简单讲就是微信开发者工具、iOS 真机和云数据库对日期对象的序列化处理不一致,字符串最稳定。第二个是 _openid,这是云开发自动加的字段,写入时如果不手动指定,云数据库会自动把它设为调用者的 openid。

索引设计是另一个必调的参数。在云开发控制台给集合加一个联合唯一索引,字段是 _openid + date,这样同一个用户同一天只能写入一条记录。这个索引能挡住大部分重复提交的问题,比前端做任何校验都可靠。

2.3 页面栈与 tabBar:为什么打卡页要放在首页而不是二级页

页面结构能看出作者对业务的理解。打卡签到类小程序,用户每天打开的第一诉求就是“今天打了没”,所以打卡页应该放在 tabBar 首页。app.json 里典型的配置是这样的:

{ "pages": [ "pages/index/index", "pages/history/history", "pages/mine/mine" ], "tabBar": { "color": "#999999", "selectedColor": "#1296db", "list": [ { "pagePath": "pages/index/index", "text": "打卡" }, { "pagePath": "pages/history/history", "text": "记录" }, { "pagePath": "pages/mine/mine", "text": "我的" } ] } }

如果你拿到的源码把打卡页放在二级页面,说明原作者可能只是做了个演示,业务上不合理。这种页面层级直接决定了用户每天要额外点一次进入,留存会差很多。

还有一个很容易踩的适配点:顶部导航栏高度。不少老源码里会把自定义导航栏高度写死成 64px,结果在带刘海屏的 iPhone 上直接顶到状态栏。正确做法是用 wx.getWindowInfo() 获取状态栏高度,再用胶囊按钮位置动态计算导航栏高度,不同机型都不一样。这个细节在改源码时值得花十分钟统一处理掉,不然测试机一换就翻车。

3. 本地跑通易打卡源码:导入、编译、真机预览的三步落地

拿到压缩包后,理论上三件事就能跑起来:解压导入、开通云开发、配好域名。但每一步都有一个最常见的卡点,我按顺序拆开讲。

3.1 用微信开发者工具导入源码:项目结构与 AppID 配置

先把 zip 解压到本地目录。这类微信小程序源码解压后,你大概率会看到一个 miniprogram 目录、一个 cloudfunctions 目录,以及 project.config.json。miniprogram 是前端页面,cloudfunctions 是云函数。如果你的包没有 cloudfunctions 目录,说明它走的是自建后端,需要你自己准备服务器接口。

打开微信开发者工具,选择“导入项目”,目录指向解压后的文件夹。这里有个关键选择:AppID 用测试号还是自己的?测试号可以编译预览,但用不了云开发。如果你打算跑通完整的前后端流程,必须先注册一个小程序账号,拿到自己的 AppID。个人主体也可以注册,这部分不复杂,在微信公众平台里按引导走就行。

导入时还容易忽略 project.config.json 里的 appid 字段。如果你发现导入后工具提示“appid 不合法”,先检查这个文件里是否写死了别人的 AppID,改成自己的再重新编译。这个文件里还藏着 libVersion 基础库版本,建议至少在 2.30.0 以上,不然现在的云开发 API 可能不兼容。

3.2 开通云开发环境并与前端完成第一次联调

绝大多数“易打卡”类源码用的是云开发,因为它免去了买服务器和备案的麻烦。开通方式:开发者工具左上角“云开发”按钮,按提示开通,然后创建一个环境,记下环境 ID。

然后看 app.js,把 wx.cloud.init 里的 env 改成你自己的环境 ID:

App({ onLaunch() { if (!wx.cloud) { console.error('基础库版本过低,请使用 2.2.3 以上基础库') return } wx.cloud.init({ env: 'your-env-id', // 替换成云开发控制台里的环境 ID traceUser: true }) } })

参数 traceUser 的作用是记录每个用户访问云开发的日志,用于调试,正式环境也可以开着,影响不大。

接着处理 cloudfunctions 目录里的云函数。在开发者工具中右键云函数文件夹,选择“创建并部署:云端安装依赖”。这一步很关键,新手在这里经常踩坑——只是本地改了云函数代码,忘了部署,前端调用一直报错。部署完成后,云开发控制台里能看到函数列表。

第一次联调可以先在开发者工具里打开调试器的 Console,调用登录云函数,看到返回的 openid 就说明链路通了。如果报错,优先看云函数日志,不要只看前端报错,云函数日志会显示更具体的异常信息。

3.3 把 request 域名配好:从开发调试到真机可用的必经之路

如果你用的源码是自建后端 API,这一步避不开。开发者工具默认会校验 request 合法域名,开发阶段可以临时在“详情 -> 本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,这样能跑但真机预览和线上版本依然会被拦截。

正式配置路径在微信公众平台:开发管理 -> 开发设置 -> 服务器域名。把后端接口域名填进“request 合法域名”。这里有三个硬性要求:域名需要 ICP 备案、必须用 HTTPS、证书要有效。本地开发机器上用 IP 访问的方式在线上是不可行的,所以后端接口一定要有正式的域名和证书。

常见报错是 request:fail url not in domain list,看到这个就说明域名没配好或者你还在用 IP 地址。如果你用的是云开发 callFunction,则不需要配置 request 合法域名,因为云函数调用走的是微信内部链路,这也是很多源码选云开发的原因。

4. 改业务规则时真正要动的地方:连续签到、补卡与订阅消息

业务方最喜欢提的需求就是改打卡规则:连续七天有奖励、漏了一天要能补卡、每天晚上提醒大家别忘打卡。这三块分别对应连续签到状态计算、补卡窗口控制、订阅消息触发,任何一个改不好都会让产品上线后挨骂。

4.1 连续签到的判定逻辑:日期连续性与状态机

先理清楚“连续签到”的确切语义:用户从今天往回数,中间没有断档的天数。如果今天还没打卡,通常从昨天开始往回数,直到遇到第一个断档日。

一个稳妥的前端计算函数是这样写的:

function calcStreak(records, today) { // records: 已打卡日期字符串数组,如 ['2026-02-18', '2026-02-17'] // today: 服务端返回的“今天”日期字符串 let streak = 0 let cursor = today // 如果今天还没打卡,从昨天开始算 if (!records.includes(today)) { cursor = getOffsetDate(today, -1) } while (records.includes(cursor)) { streak++ cursor = getOffsetDate(cursor, -1) } return streak }

getOffsetDate 的实现建议用字符串解析后做日期偏移,不要直接用字符串拼接去算前一天。跨月和跨年的时候字符串拼写很容易出错,比如 2026-03-01 前一天应该是 2026-02-28,用 Date 操作才靠谱:

function getOffsetDate(dateStr, offset) { const parts = dateStr.split('-').map(Number) const d = new Date(parts[0], parts[1] - 1, parts[2]) d.setDate(d.getDate() + offset) const m = String(d.getMonth() + 1).padStart(2, '0') const day = String(d.getDate()).padStart(2, '0') return `${d.getFullYear()}-${m}-${day}` }

这里有一个容易搞错的参数:today 不要用前端 new Date() 生成,而应该用服务端或云函数返回的时间。原因很简单,用户设备时间可能不准,也可能故意改时间。云函数里的服务端时间才是可信的,前端只负责展示。

连续签到状态不要在前端持久化,只存打卡记录本身,连续天数每次实时计算。这样即使你改了计算规则,历史数据也不会坏。

4.2 补卡规则与截止时间的边界参数

补卡的实现逻辑比连续签到容易,但边界条件多。常见规则是“允许补最近 3 天的卡”,这个“3 天”就是核心参数,我一般把它集中定义,方便后续改:

const ALLOW_MAKEUP_DAYS = 3 function canMakeup(dateStr, todayStr) { const today = parseDate(todayStr) const target = parseDate(dateStr) const diffDays = Math.floor((today.getTime() - target.getTime()) / 86400000) return diffDays >= 0 && diffDays < ALLOW_MAKEUP_DAYS }

注意这里的 diffDays 计算有坑:直接相减除以一天的毫秒数,在跨夏令时地区会差一小时导致算错。中国大陆没有夏令时,问题不大,但如果你的团队有海外成员,建议用 UTC 日期先做归一化再算。

补卡界面一般会让用户选择具体日期,这时小程序的单选框或日历弹层组件就派上用场。日期选择器可以用 picker 的 mode=date,也可以自己画一个日历面板。自己画的时候要注意,面板上能选的日子必须经过 canMakeup 过滤,不能选的置灰。有些源码为了省事把补卡写成“补昨天”,这种设计在周末就翻车,因为用户周一打开想补上周五的卡,却被逻辑拒绝了。

补卡本质上也是一次写入,建议和正常打卡共用同一个写入云函数,用一个 type 字段区分是 normal 还是 makeup,方便后续做统计。

4.3 订阅消息的模板选用与推送时机

每日提醒是打卡类小程序的灵魂,但微信的订阅消息机制让这个功能很容易做歪。首先要在微信公众平台申请订阅消息模板,比如“打卡提醒”类目下的模板,拿到 templateId。

前端触发订阅的代码一般嵌在某个按钮事件里:

wx.requestSubscribeMessage({ tmplIds: ['模板ID'], success(res) { if (res.errMsg === 'requestSubscribeMessage:ok') { // 用户点了同意,服务端之后才能给 ta 推一条消息 wx.showToast({ title: '提醒开启成功', icon: 'success' }) } else if (res.errMsg === 'requestSubscribeMessage:fail cancel') { wx.showToast({ title: '取消订阅', icon: 'none' }) } } })

这里最容易被无视的坑是:一次性订阅消息授权一次,只能推一条。你今天让用户点了同意,明天再推就需要用户再点一次授权。想在每天固定时间自动提醒,就得每天引导用户回来点一次“开启明日提醒”,这种交互对产品设计的要求很高。另一条路是长期订阅消息,但目前只对部分类目开放,不是所有小程序都能申请到。如果你做的是企业内部工具,可以考虑用企业微信的应用消息做触达,这个不受订阅消息次数限制。

推送时机的参数也很关键。我一般建议设在工作开始前半小时,比如团队 9 点打卡,就 8 点 30 分推送。云开发可以用定时触发器实现,云函数里配置一个触发规则,每天固定时间去查未打卡名单,逐个调 subscribeMessage.send。要特别注意服务端推送失败的错误码,比如 43101 表示用户未订阅或订阅次数耗尽,这种情况要静默忽略,不要报错堆满日志。

5. 易打卡源码高频踩坑:现象、原因与可复现的解决步骤

打卡源码本身不算复杂,但运行环境涉及工具、真机、云端三方,任何一个环节都会出“黑匣子”问题。我挑四个在开发群里见到最多次的坑,按“现象 -> 原因 -> 解决”的方式说清楚。

5.1 真机预览白屏:先查基础库版本和合法域名

现象:开发者工具编译预览一切正常,手机上扫码打开一片白屏,Console 有红色报错。

原因:最常见是三种。一是项目的基础库版本设置过低,开发者工具默认基础库可能和真机不一致,云开发在部分旧基础库上不可用。二是 app.js 里 wx.cloud.init 的 env 还没有替换成你自己的环境 ID,云函数调用全部失败。三是自建后端模式没有配合法域名,前端请求被拦截,页面初始化拿不到数据就白屏了。

解决:按顺序排查。先看开发者工具右上角“详情 -> 本地设置 -> 调试基础库”,确认在 2.30.0 以上。再看 app.js 里的 env 是否指向你自己的云开发环境。最后在真机上打开调试模式(右上角菜单打开调试),白屏时直接看 vConsole 的报错信息,报错里会明确写是 url not in domain list 还是云开发初始化失败。不要在真机没开调试的模式下盲猜,那等于闭着眼睛修车。

5.2 日历显示“昨天”和“今天”错位:时区与 new Date() 的坑

现象:打卡日历上今天的位置高亮不对,或者从历史记录返回时日期跳了一天。

原因:前端代码里如果用 new Date('2026-02-18') 这样的写法,日期字符串在 iOS 真机上会被解析成 UTC 时间,而中国时区是 UTC+8,于是 2026-02-18 变成了 2026-02-18 早上 8 点,如果此时本地时间是凌晨,就会错位。开发者工具和安卓机对日期字符串的处理略有差异,这属于典型的“工具上好的,真机就坏”。

解决:统一用我前面写过的 parseDate 方式,把日期字符串拆开,分别传给 Date 构造函数:

function parseDate(dateStr) { const parts = dateStr.split('-').map(Number) return new Date(parts[0], parts[1] - 1, parts[2]) }

同时约定所有接口返回日期都用“YYYY-MM-DD”字符串,不在前端做时区换算。只要统一了这个基准,日历绘制和连续签到计算都会稳定下来。

5.3 重复打卡与重复提交:按钮防抖和数据库唯一索引

现象:用户手快连点两下打卡按钮,数据库里出现了两条同一天同用户的记录,打卡日历上显示一个裂开的样式,统计数据也翻倍。

原因:单纯用前端按钮 loading 可以挡住大部分重复点击,但接口超时后用户重新点一下,或者两个请求并发同时到达云函数,前端 loading 状态根本挡不住第二批请求。

解决:双管齐下。前端在按钮点击后立即进入 loading 状态,并禁用按钮:

setLoading(true) try { const res = await wx.cloud.callFunction({ name: 'checkin', data: { date: todayStr } }) if (res.result.success) { // 刷新打卡状态 } } finally { setLoading(false) }

后端云函数里则必须做幂等校验:先根据 _openid 和 date 查一次,存在就直接返回“已打卡”,不重复写入。再在云开发控制台给 checkins 集合加 _openid + date 联合唯一索引,双保险。加了唯一索引后,如果云函数没有先查询就插入,会报重复键错误,需要在云函数里 catch 住这种情况,让用户看到友好提示而不是程序崩溃。

5.4 手机号获取失败:getPhoneNumber 的触发条件与风险提示

现象:点击页面上的手机号授权按钮,没有任何反应,或者直接报 getPhoneNumber:fail no permission。

原因:这个功能有硬性条件。第一,必须用 button 组件且 open-type 为 getPhoneNumber,不是任何自定义 view 都能触发。第二,小程序必须是已认证的主体,个人主体小程序在大部分情况下拿不到完整手机号。第三,基础库版本低了也会失败。

常见的正确写法是:

<button open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber"> 获取手机号 </button>
onGetPhoneNumber(e) { if (e.detail.errMsg === 'getPhoneNumber:ok') { // e.detail.code 是动态令牌,需要发给后端换手机号 wx.cloud.callFunction({ name: 'getPhone', data: { code: e.detail.code } }) } else { wx.showToast({ title: '未授权', icon: 'none' }) } }

需要提醒的是:2023 年之后微信调整了规则,动态获取手机号的接口只对认证企业主体开放。个人开发者在做内部打卡工具时,不要依赖手机号作为唯一标识,直接用 openid 存数据更实际。如果你确实需要手机号联系用户,可以在打卡后让用户手动填写,这个不受限制。很多“易打卡”源码里的手机号登录功能,在你自己的账号下跑不通,不一定是代码坏了,而是主体类型不满足条件。

6. 把这套源码改造成团队周报打卡:扩展字段、统计与导出

打卡签到的骨架搭好后,扩展成一个按周提交的团队周报并不难。做法很直接:不改打卡的日期判定逻辑,只给记录追加字段,把当天提交的内容从“签到”变成“今日进展 + 明日计划”。

6.1 在打卡记录上扩展自定义字段:从“打卡”到“周报”的最小改动

数据库里原本的记录有 date、remark、type 三个字段。现在加一个 content 字段存周报正文,再加一个 images 数组存图片,表单页把 remark 的输入框换成 textarea,加上 wx.chooseMedia 的图片选择:

async onSaveReport() { const content = this.data.content.trim() if (!content) { wx.showToast({ title: '内容不能为空', icon: 'none' }) return } await wx.cloud.callFunction({ name: 'checkin', data: { date: this.data.todayStr, content, images: this.data.imageList, type: 'weekly' } }) }

前端改动就这么多,后端云函数里的写入逻辑也不需要大改,把 event 里的 content 和 images 原样存进去即可。这类源码的扩展性就在这里体现出来:数据结构保持简单,字段可以随时加,不要因为想兼容所有业务就提前设计一堆冗余字段。

6.2 用数据统计验证改造是否正确:实时校验的脚本思路

改完以后要验证,不是人工点一遍就算完。我一般会在云开发控制台写一个临时的统计云函数,检查当天提交记录的数量、用户分布和字段完整性:

exports.main = async (event) => { const db = cloud.database() const { date } = event const res = await db.collection('checkins') .where({ date, type: 'weekly' }) .count() const sample = await db.collection('checkins') .where({ date }) .limit(5) .get() return { count: res.total, sample: sample.data.map(item => ({ date: item.date, hasContent: !!item.content, userMasked: item._openid.slice(0, 8) })) } }

这个脚本的价值是让你在控制台直接看到数据长什么样,而不是每次都要打开小程序去猜。验证的维度主要是三个:提交内容有没有丢失、是否覆盖了所有打卡用户、日期字段有没有异常。

我自己的习惯是每改一次业务规则,就先把旧的验证路径在代码里走一遍,确认数据格式没问题再发布。早期做打卡功能时,因为图省事跳过了一次验证,结果上线后发现某个版本的数据没有 content 字段,导致后端的周报统计全部错位,连夜补数据才缓过来。从那以后,验证脚本就成了每一次改动的固定动作。希望这份源码踩坑经验能帮你少走这段弯路。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进