简介:易打卡签到微信小程序完整源码包,面向小程序入门者与需要参考完整业务场景的移动端学习者,用于理解签到功能从页面到数据的实现。压缩包共81个文件,以JavaScript逻辑、WXML页面结构、WXSS样式、JSON配置和PNG图片为主,另有打包说明与效果动图,整体仅1.51MB,便于快速下载与本地调试。目前已有895人浏览学习。源码按标准小程序工程划分,包含全局应用逻辑、全局配置和公共样式,pages目录下含签到与个人记录页面,utils工具函数提供日期处理等辅助能力,并封装了网络请求模块,还附带第三方依赖库。通过分析这套源码,可掌握用户登录状态管理、签到规则实现、服务端数据交互和缓存调用等关键开发技巧,也能熟悉页面生命周期、本地缓存及API调用方式,适合在此基础上扩展个人功能或用于课程设计参考。
1. 易打卡签到案例源码:先从“点一下打卡”这个需求说起
部门群每天靠接龙打卡,月底统计时打开聊天记录翻得人想辞职,这是很多公司和小团队的日常。这套《易打卡签到案例源码》解决的正是这个高频需求:一个能拿定位、防重复、统计连续签到天数的微信小程序。它属于微信小程序开发里典型“小而全”的案例,源码包内包含小程序前端页面、云函数与数据库设计,适合刚跑通 Hello World、想完整上手一个真实项目的新手,也适合要快速交付同类考勤需求的人。我的判断和初看相反:打卡功能表面只有“点一下”,真要上线不怕翻车,坑全在定位授权、时间时区和重复提交这三件小事上。
2. 从源码包看易打卡的工程结构:页面、云函数、数据库怎么分工
拿到源码第一步不是打开微信开发者工具,而是先看目录。这个案例的典型布局是原生微信小程序,不是 uniapp 那类编译型工程。原生工程的好处是依赖少、启动快、改起来直接;坏处是如果作者没有维护好目录,你会很快迷失在页面回调里。先花十分钟把文件归属搞清楚,后面每一步都不会跑偏。
2.1 源码目录拆解:小程序前台与云函数各自负责什么
易打卡签到案例/ │ ├── cloudfunctions/ # 云函数目录,不能直接本地运行 │ ├── checkIn/ # 打卡云函数:写打卡记录、判重、算距离 │ ├── getRecords/ # 查询云函数:拉取打卡记录列表 │ └── stat/ # 统计云函数:连续签到、日报汇总 │ ├── pages/ │ ├── index/ # 首页:打卡按钮、当前位置、打卡点距离 │ ├── record/ # 记录页:按日期展示打卡历史 │ └── mine/ # 我的:个人信息、统计入口 │ ├── utils/ │ ├── util.js # 距离计算、日期格式化等工具函数 │ └── config.js # 打卡点坐标、半径阈值、时间窗等常量 │ ├── app.js # 全局逻辑,云环境初始化在这里 ├── app.json # 全局配置,包含页面路由与权限声明 ├── app.wxss # 全局样式 └── project.config.json # 项目配置:AppID、云函数目录声明这是原生微信小程序开发里最常见的结构。注意cloudfunctions目录下的每个子目录是独立的 Node.js 云函数,不能像普通 JS 文件一样被页面直接 import。它们要经过“上传并部署”这个动作,才能被wx.cloud.callFunction调用。很多新手在本地改了云函数代码,然后发现线上行为没变,就是因为漏了这一步。
utils/config.js是整份源码里最该放开改的文件:打卡点经纬度、打卡半径、允许打卡的时间窗、公司名称,全部收敛在这里。我见过不少源码把半径阈值直接写在页面data里,后期想从 100 米调整到 300 米要翻三个页面,改完还容易漏一处。拿到源码先找这个文件,把里面的常量读一遍,你会比作者更快理解这个项目。
2.2 打卡主流程的数据流:从点击按钮到写库记录
// pages/index/index.js 节选 const config = require('../../utils/config.js') Page({ data: { checking: false, // 防抖标志,防止连点 location: null }, async onTapCheckIn() { if (this.data.checking) return this.setData({ checking: true }) try { // 1. 先拿定位,再调云函数 const { latitude, longitude } = await this.getLocation() // 2. 云函数负责判重、算距离、写库 const res = await wx.cloud.callFunction({ name: 'checkIn', data: { latitude, longitude } }) // 3. 统一约定返回结构 { ok, msg } if (res.result.ok) { wx.showToast({ title: '打卡成功', icon: 'success' }) } else { wx.showToast({ title: res.result.msg, icon: 'none' }) } } catch (e) { wx.showToast({ title: '打卡失败:' + e.message, icon: 'none' }) } finally { this.setData({ checking: false }) } }, getLocation() { return new Promise((resolve, reject) => { wx.getLocation({ type: 'gcj02', // 坐标系必须统一,后面第 4 章细说 success: resolve, fail: reject }) }) } })这段代码的要点不是wx.cloud.callFunction本身,而是前后端责任的切分。页面只做三件事:拿定位、传参数、展示结果。真正该做的判重、距离计算、数据落库全部在云函数里。这也是云开发项目的推荐写法——页面直连数据库虽然快,但集合权限很难配置成“每个用户只能读写自己的记录”,云函数统一走管理端权限,天然绕开了这个头疼的问题。
checking这个布尔标志是防抖的关键。没有它的保护,用户在弱网环境下双击按钮,两条云函数请求几乎同时到达后端,判重逻辑就可能被绕过,于是出现一个人一天打了两次卡的脏数据。
2.3 签到状态存储:云数据库集合还是本地缓存
在动手跑项目前,先理解数据放哪儿。这个案例有两种常见存储做法,选型直接决定你能统计到什么程度:
| 存储方式 | 适用场景 | 主要缺点 |
|---|---|---|
| 云数据库集合 | 多人打卡、需要日报/月报统计 | 需要理解云开发的基础概念与额度 |
| 本地缓存 storage | 单人演示、纯前端跑通 | 换手机或删小程序后数据全丢 |
案例源码一般会选云数据库,否则“打卡记录”跨设备看不到,连续签到统计也没意义。核心集合设计是这样一张checkin_log:
// checkin_log 集合文档结构(不是代码,是字段约定) { openid: 'oXk...', // 微信 openid,用户唯一标识 date: '2025-03-20', // 打卡日期,字符串格式 YYYY-MM-DD time: 1742400000000, // 打卡时间戳(毫秒),用于列表排序 location: { // 打卡时的经纬度 lat: 31.2304, lng: 121.4737 }, distance: 128, // 距打卡点的距离,单位米 status: 'ok' // 打卡结果 }date用字符串而不是时间戳,是因为后面要按天查重、按天统计,字符串精确匹配比区间查询简单直接。time单独存毫秒时间戳,用来做当天多时间段排序或补卡逻辑。openid不建议再包一层user_id映射,在个人主体小程序的场景下 openid 已经够用了。
集合建好之后,记得去云开发控制台给checkin_log建一个openid + date的联合唯一索引。这个索引在数据库层面兜底防重复,是“云函数先查后插”之外的第二道保险。具体怎么建,第 5 章的避坑部分会展开。
3. 把源码跑起来:导入项目、初始化云环境、部署首个云函数
很多人在这一步卡住,不是因为代码有问题,而是环境没有对齐。微信开发者工具版本、云环境 ID、集合权限,三样缺一样都可能让你误判源码有问题。按顺序做,每一步都确认绿色通过再往下走。
3.1 用微信开发者工具导入项目的三个前置条件
第一个前置条件:把 zip 解压到纯英文路径。C:\Users\张三\桌面\易打卡这种路径在小程序编译时偶尔会触发奇怪的编码问题,不是必然,但没必要赌。第二个前置条件:去微信公众平台注册一个小程序账号,个人主体即可,审核很快,注册后拿到 AppID。第三个前置条件:安装最新稳定版微信开发者工具,导入时选择“小程序”项目类型,不是“小游戏”。
拿到 AppID 后导入源码。如果源码里带了project.config.json,工具会直接识别;如果导入后工具提示找不到app.json,大概率是 zip 包解压后多套了一层目录,把内层项目文件夹作为根目录重新导入就行。
// project.config.json 关键字段 { "miniprogramRoot": "miniprogram/", "cloudfunctionRoot": "cloudfunctions/", "appid": "你的AppID", "setting": { "urlCheck": true, "es6": true, "enhance": true } }这里有个值得记住的细节:cloudfunctionRoot写错成cloudFunctions(中间多一个大写 S),开发者工具会直接不识别云函数目录。如果源码里没有这两个字段,建议先用工具新建一个空白项目,再把这些配置粘回去,比手打可靠得多。
3.2 初始化云开发环境:创建环境、建立集合、设置权限
项目导入成功之后,进入开发者工具,点击工具栏的“云开发”按钮。第一次使用会让你创建云环境,注意这里要记住的是环境 ID,不是环境名称。环境 ID 是后续代码里wx.cloud.init要填的字符串,一旦确定不建议频繁修改,因为它会出现在所有云函数和数据库配置里。
环境创建完成后,在云开发控制台里建两个集合:checkin_log(打卡日志)和checkin_points(打卡点配置)。集合权限统一选择“仅创建者可读写”。这个选项看起来会让云函数写入失败,实际上不会——云函数运行时不带用户身份,直接走管理端权限,不受集合权限限制。页面端反而因此变得更安全,因为用户只能读到自己创建的数据。
// app.js 全局云环境初始化 App({ onLaunch() { if (!wx.cloud) { console.error('当前基础库过低,请使用 2.2.3 以上版本') return } wx.cloud.init({ env: 'checkin-prod', // 环境 ID,不是环境名称 traceUser: true // 在云函数日志中记录 openid,方便排查 }) } })traceUser: true会带来一点性能损耗,但换来的是云函数日志里每条调用都带用户 openid。排查“谁在什么时间触发打卡失败”时,这个字段比任何断点都管用。线上跑稳定之后如果在意性能,可以关掉。
3.3 最小可运行的打卡脚本:checkIn 云函数的核心代码与参数说明
// cloudfunctions/checkIn/index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() exports.main = async (event) => { // 云函数上下文里能拿到可信的 openid,不能信任客户端传入的用户标识 const { OPENID } = cloud.getWXContext() // 日期由云函数生成,但要显式按北京时间处理 const date = event.date || formatDate(new Date()) const col = db.collection('checkin_log') // 先查当天是否已打卡,防重复的第一道闸门 const existed = await col.where({ openid: OPENID, date }).count() if (existed.total > 0) { return { ok: false, msg: '今天已打卡' } } // 距离计算在调用方已做过一次,这里再存一份结果用于报表 await col.add({ data: { openid: OPENID, date, time: Date.now(), location: event.location || null, status: 'ok' } }) return { ok: true, msg: '打卡成功' } } function formatDate(d) { // 云函数运行环境是 UTC 时区,直接用 toISOString 会在 22 点后把日期切成第二天 const t = new Date(d.getTime() + 8 * 3600 * 1000) return t.toISOString().slice(0, 10) }这段代码有两个必须守住的原则。第一,openid 一定从cloud.getWXContext()拿,不要接收客户端传上来的openid参数,因为那是可以伪造的。第二,日期格式化必须加上 8 小时偏移再截取,否则 UTC 时区会让每天 22:00 到 24:00 的打卡记录落到“明天”。我在第 5 章会再讲这个坑的展开版本。
部署时在开发者工具左侧文件数里右键checkIn目录,选择“上传并部署:云端安装依赖”。等状态变成绿色,你的最小闭环就通了:页面调云函数,云函数写数据库,数据库返回结果。接下来可以测试定位与距离判定了。
4. 打卡核心逻辑拆解:定位授权、范围判定与连续签到统计
打卡比签到多出来的一块核心复杂度就是位置。拿到经纬度只是第一步,授权、坐标系、距离阈值、边界情况处理,任何一个环节没做对,用户在真实门口就会反复失败。这一章把打卡背后的四段逻辑拆开讲透。
4.1 获取定位授权与经纬度:wx.getLocation 的参数与坐标系
// utils/location.js 封装定位逻辑 function getLocation() { return new Promise((resolve, reject) => { wx.getLocation({ type: 'gcj02', // 必须用 gcj02,国测局坐标系 isHighAccuracy: true, // 开启高精度,基础库 2.9.0+ highAccuracyExpireTime: 4000, // 高精度定位最长等待时间,毫秒 success: resolve, fail: (err) => { // 统一处理授权被拒的场景 if (err.errMsg && err.errMsg.includes('auth deny')) { wx.showModal({ title: '需要定位权限', content: '请在设置中允许使用位置信息,否则无法打卡', confirmText: '去设置', success: (m) => { if (m.confirm) wx.openSetting() } }) } reject(err) } }) }) }这里的第一个坑是坐标系。wx.getLocation的type参数有wgs84和gcj02两种,默认是wgs84。但国内地图服务(包括腾讯地图、高德地图)使用的都是gcj02。如果打卡点坐标是在小程序地图组件上拾取的,而定位用的是默认wgs84,算出来的距离会偏差几十到几百米,足够让一个站在办公室里的人被判定为“不在范围”。案例源码里如果要用地图组件展示打卡位置,建议统一使用gcj02。
第二个坑是隐私声明。2022 年之后,微信对地理位置这类隐私接口查得极严。单写wx.getLocation不够,还必须在app.json里同时声明:
{ "permission": { "scope.userLocation": { "desc": "用于记录打卡位置,确认到达签到范围" } }, "requiredPrivateInfos": ["getLocation"] }缺少这一段,真机上定位接口会直接失败,而开发者工具里一切正常。这是最容易让人误判源码有问题的场景。
4.2 打卡范围判定:Haversine 距离计算与半径阈值设置
拿到经纬度之后,下一步是算它跟打卡点的距离。地图上的经纬度是球面坐标,不能用平面上那套Math.sqrt((x1-x2)**2 + (y1-y2)**2)直接算。常见做法是用 Haversine 公式,代码量不大,足够满足打卡场景的精度要求:
// utils/util.js 球面距离计算 function getDistance(lat1, lng1, lat2, lng2) { const R = 6371000 // 地球平均半径,单位米 const rad = Math.PI / 180 const dLat = (lat2 - lat1) * rad const dLng = (lng2 - lng1) * rad const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1 * rad) * Math.cos(lat2 * rad) * Math.sin(dLng / 2) * Math.sin(dLng / 2) return 2 * R * Math.asin(Math.sqrt(a)) } // pages/index/index.js 打卡按钮内部判定 const config = require('../../utils/config.js') function checkDistance(lat, lng) { const point = config.checkPoint // 从 config.js 读取,不在页面写死 const distance = getDistance(lat, lng, point.lat, point.lng) if (distance > config.radius) { return { ok: false, msg: `距打卡点 ${Math.round(distance)} 米,超出范围` } } return { ok: true, distance } }半径阈值怎么定,取决于你的实际场景。办公室室内定位,GPS 漂移 50 到 100 米是常态,阈值设 100 米基本够用;园区或学校操场建议 200 到 300 米;工厂厂区如果打卡点设在门卫室,300 米也可以接受。我的一般建议是:先用 150 米上线跑一周,看后台记录的distance分布,有多少人徘徊在 200 米上下,再把阈值调成那个值加上 50 米冗余。别把半径压得太死,否则会收到“我明明在门口却打不了卡”的投诉。
4.3 连续签到统计:按日期去重的数据设计
连续签到天数是打卡类小程序最让用户兴奋的功能,但实现它不需要复杂算法。关键在数据设计时给date字段留了位置。因为每条记录限定openid + date唯一,统计时就只需要做字符串集合运算:
// cloudfunctions/stat/index.js 节选 const db = cloud.database() exports.main = async (event) => { const { OPENID } = cloud.getWXContext() // 拉最近 90 天打卡记录,一天最多一条 const res = await db.collection('checkin_log') .where({ openid: OPENID }) .orderBy('date', 'desc') .limit(90) .get() const daySet = new Set(res.data.map(item => item.date)) let streak = 0 // 如果今天已打卡,从今天往前数;没打卡则从昨天往前数 const today = formatDate(new Date()) let cursor = daySet.has(today) ? today : formatDate(new Date(Date.now() - 86400000)) while (daySet.has(cursor)) { streak++ const t = new Date(cursor + 'T00:00:00') t.setDate(t.getDate() - 1) cursor = formatDate(t) } return { ok: true, streak } } function formatDate(d) { const t = new Date(d.getTime() + 8 * 3600 * 1000) return t.toISOString().slice(0, 10) }这里有两个边界值得注意。第一,date是用“北京时间”生成的,所以统计端也必须用同一套formatDate逻辑,不能一会儿 UTC 一会儿北京。第二,“今天没打卡”不能直接断掉连续天数,否则用户晚上还没打,早上的连续天数就归零了,体验很差。从昨天开始数,是打卡应用的惯例。
5. 源码改完再上线的避坑指南:五条高频翻车记录
这个案例看起来功能简单,但它是云开发 + 定位 + 定时统计的组合体,实际踩坑点比普通展示型小程序多得多。以下是五条我在交付类似项目时踩过、排过、最终沉淀下来的问题,每条按“现象 → 原因 → 解决”展开。
5.1 定位相关:真机转圈与坐标系混用
现象 1:开发者工具里点打卡立刻成功,一到真机就转圈,几秒后提示“定位失败”。
原因:开发者工具默认模拟定位,不需要走隐私接口授权链路;真机上wx.getLocation是隐私接口,若app.json缺少requiredPrivateInfos、或小程序后台未配置“用户隐私保护指引”,基础库会直接拒绝调用。
解决:在app.json补上第 4.1 节那两段配置;同时登录微信公众平台,在小程序后台的“设置 → 服务内容声明 → 用户隐私保护指引”中声明“位置信息”用途。改完后删掉小程序重新编译,让隐私协议弹窗重新出现。想要快速确认授权是否通过,可以在真机上打开调试器的 vConsole,查看getLocation的 fail 回调里是否出现privacy permission is not authorized。
现象 2:打卡点明明就在面前,却提示“距打卡点 800 米,超出范围”。
原因:坐标系混用了。微信定位返回的wgs84和gcj02相差数百米;或者打卡点坐标是从某个 GPS 设备导出的 WGS-84 数据,而定位用的是 GCJ-02。两者混用,距离就会整体漂移。
解决:全部统一到gcj02。打卡点坐标如果是从腾讯地图坐标拾取器或者小程序地图组件上点的,那就是 GCJ-02,直接用;如果拿到的是原始 WGS-84 坐标,先做一次坐标转换再存库,不要直接塞进config.js。
5.2 数据相关:重复打卡与 UTC 时区
现象 3:云函数已经做了“先查再写”,但数据库里还是出现同一个人同一天两条打卡记录。
原因:“先查再写”只防串行,不防并发。用户在弱网下双击,两条云函数请求几乎同时到达,两个实例都执行了count(),都得到 0,然后都执行add()。
解决:两层兜底。前端加checking防抖;数据库层在云开发控制台为checkin_log建openid + date联合唯一索引。索引冲突会让第二条add()直接抛错,云函数里 catch 住这个错误,把它转成“今天已打卡”的提示即可。这是唯一能完全堵住并发重复的方案。
现象 4:用户 23:30 打卡,后台显示的日期却是第二天。
原因:云函数运行环境是 UTC 时区,北京时间比 UTC 快 8 小时。开发者工具本地调试时云函数跑在你的机器上,时区是北京时间,看不出问题;一上传到云端,new Date().toISOString()返回的就是 UTC 日期,22 点之后被切成“明天”。
解决:方案一是第 3 章代码里的做法:云函数生成日期时手动加 8 小时再取年月日。方案二更稳:日期字符串由小程序端生成后作为参数传给云函数,因为用户手机时区基本就是北京时间。我自己写到后面都倾向方案二,云端只做校验不生成日期,少一处时区黑匣子。
5.3 云函数与部署限制:调用失败与手机号登录
现象 5:wx.cloud.callFunction报errCode: -501000或FunctionName parameter could not be found。
原因:常见三种:云函数压根没上传部署;app.js里wx.cloud.init的env填了环境名称而不是环境 ID;project.config.json里的cloudfunctionRoot配置错误,导致开发者工具不认为cloudfunctions是云函数目录。
解决:先打开app.js确认env是环境 ID(一串小写字母加数字,不是中文名称)。再检查project.config.json里有cloudfunctionRoot字段。最后在开发者工具里右键checkIn目录,选择“上传并部署:云端安装依赖”,等待状态变绿。如果还需要定位,可以临时给云函数加一行console.log(process.env.TZ)来确认时区。
现象 6:案例里用了“微信手机号快速验证”按钮,点击后弹不出手机号授权。
原因:获取手机号接口需要小程序主体为企业类型并完成微信认证,个人主体小程序没有这个能力。即便主体满足,也需要在小程序后台开通对应接口权限。
解决:个人开发者的正确做法是放弃手机号登录,直接用cloud.getWXContext().OPENID作为用户唯一标识。打卡场景不需要手机号,openid 足够支撑“个人中心 + 打卡记录 + 连续天数”全部功能。如果业务上必须绑手机号,建议先确认主体资质,再决定是否值得为这个接口做企业认证。
6. 案例升级的三个具体改动:把易打卡变成团队考勤工具
跑通不代表能用,用起来和能交付是两回事。这里给你三条最实用的升级路线,每一段代码都能直接补进现有源码。
6.1 打卡点从写死改成数据库配置
config.js里的单打卡点只适合个人练习。真实团队考勤往往有多个打卡点,而且管理员应该能在后台改,不用发版。把打卡点迁到checkin_points集合,然后checkIn云函数里改用“遍历最近打卡点”的方式判定:
// checkIn 云函数中新增距离匹配逻辑 const pointsRes = await db.collection('checkin_points').limit(100).get() const points = pointsRes.data let nearest = null let minDist = Infinity for (const p of points) { const d = getDistance(lat, lng, p.lat, p.lng) if (d < minDist) { minDist = d nearest = p } } if (!nearest || minDist > (nearest.radius || 200)) { return { ok: false, msg: '不在任何打卡点范围内' } }打卡点数据结构里加一个radius字段,每个点可以单独设置范围。如果还想做“早班/晚班”区分,可以再加workTime字段,前端用单选组的radio组件做班次选择,云函数在写入记录时一并保存。
6.2 用云函数定时触发器生成日报
连续签到是给员工看的,日报汇总才是给管理者看的。云开发支持定时触发器,不需要额外服务器。给stat云函数加一个配置文件:
// cloudfunctions/stat/config.json { "triggers": [ { "name": "dailyStat", "type": "timer", "config": "0 0 22 * * * *" } ] }这个 cron 表达式是七段格式:秒 分 时 日 月 周 年。上面表示每天 22:00:00 执行一次。云函数内部把当天的checkin_log按打卡点分组统计,写入daily_report集合。第二天早上管理员打开小程序就能看到昨天哪些人缺卡,不用再人工对 Excel。
6.3 在管理端导出 CSV 考勤表
最终报表还是要落到表格里。小程序里生成文件不方便,但可以让云函数拼好 CSV 字符串,再通过wx.setClipboardData复制到剪贴板,用户在电脑上粘贴到 Excel 即可。记得在字符串最前面加上 UTF-8 BOM 标记,否则 Excel 打开中文会乱码:
// 管理端导出 CSV 的核心逻辑 const csvRows = ['openid,date,time,distance'] for (const row of todayRecords) { csvRows.push(`${row.openid},${row.date},${row.time},${row.distance}`) } const csv = '\ufeff' + csvRows.join('\n') wx.setClipboardData({ data: csv, success: () => wx.showToast({ title: '已复制,请粘贴到Excel', icon: 'none' }) })这套案例我前后改过三轮,第一轮三天交付觉得很简单,第二轮被客户按在地上磨了一个月,最后收敛出的改动无非就是上面这些:打卡点可配置、日报自动生成、报表能导出。最大教训是不要相信服务器自带时间,所有和日期有关的逻辑必须显式按北京时间处理;其次是并发重复打卡这种问题,只靠代码防御不够,数据库唯一索引才是后悔药。希望你拿到源码后,别急着换主题皮肤,先把第 3 章的环境步骤跑通,把第 5 章的坑提前堵上,后面省下的时间远比改 UI 值得。希望帮到你。
本文还有配套的精品资源,点击获取