概述
考勤系统是企业/学校常见需求。App Inventor 2可以实现的考勤方案:
| 方案 | 原理 | 精度 | 难度 |
|---|---|---|---|
| GPS定位打卡 | LocationSensor定位 | 10-50米 | ⭐⭐ |
| 二维码签到 | 扫码确认身份 | 高 | ⭐⭐ |
| WiFi打卡 | 连接指定WiFi | 中 | ⭐⭐⭐ |
| 蓝牙打卡 | 检测蓝牙信标 | 高 | ⭐⭐⭐ |
方案一:GPS定位打卡
原理
员工到达公司附近,点击”打卡”按钮。App获取当前GPS位置,与公司坐标比较,在范围内则打卡成功。
界面设计
Screen_Attendance
├── 标签_日期时间(Clock实时更新)
├── 标签_位置(显示经纬度)
├── 标签_距离(距公司多远)
├── 标签_状态(打卡状态)
├── 按钮_打卡
├── ListView_记录(打卡记录)
├── 位置传感器1
├── 计时器1
├── 微数据库1
└── Web客户端1(上传到服务器)
积木块代码
global 公司纬度 = 39.9042
global 公司经度 = 116.4074
global 打卡范围 = 200 // 200米内可以打卡
global 当前纬度 = 0
global 当前经度 = 0
when Screen_Attendance.Initialize() {
位置传感器1.Enabled = true
计时器1.TimerInterval = 1000
计时器1.TimerEnabled = true
标签_日期时间.Text = 计时器1.FormatDateTime(计时器1.Now(), "yyyy-MM-dd HH:mm:ss")
}
when 计时器1.Timer() {
标签_日期时间.Text = 计时器1.FormatDateTime(计时器1.Now(), "yyyy-MM-dd HH:mm:ss")
}
when 位置传感器1.LocationChanged(纬度, 经度, 海拔, 速度) {
global 当前纬度 = 纬度
global 当前经度 = 经度
标签_位置.Text = join("📍 ", formatAsDecimal(纬度, 6), ", ", formatAsDecimal(经度, 6))
// 计算距离
global 距离 = 计算距离(纬度, 经度, 公司纬度, 公司经度)
标签_距离.Text = join("距公司:", Math.Round(距离), "米")
}
when 按钮_打卡.Click() {
if 当前纬度 == 0 {
标签_状态.Text = "❌ GPS定位中,请稍候..."
} else {
global 距离 = 计算距离(当前纬度, 当前经度, 公司纬度, 公司经度)
global 当前时间 = 计时器1.FormatDateTime(计时器1.Now(), "yyyy-MM-dd HH:mm:ss")
global 今天日期 = 计时器1.FormatDateTime(计时器1.Now(), "yyyy-MM-dd")
if 距离 <= 打卡范围 {
// 在打卡范围内
global 记录 = makeDict(
pair("date", 今天日期),
pair("time", 当前时间),
pair("type", "签到"),
pair("lat", 当前纬度),
pair("lng", 当前经度),
pair("distance", Math.Round(距离))
)
// 保存到本地
global 所有记录 = 微数据库1.GetValue("attendance_records", createEmptyList())
addItemsToList(所有记录, 记录)
微数据库1.StoreValue("attendance_records", 所有记录)
// 上传到服务器
global JSON = Web客户端1.JsonTextEncode(记录)
Web客户端1.RequestHeaders = ["Content-Type: application/json"]
Web客户端1.PostText("https://api.example.com/attendance", JSON, "UTF-8")
标签_状态.Text = join("✅ 打卡成功!", 当前时间)
标签_状态.TextColor = "green"
刷新记录列表()
} else {
标签_状态.Text = join("❌ 不在打卡范围内(距公司", Math.Round(距离), "米)")
标签_状态.TextColor = "red"
}
}
}
Haversine距离计算
function 计算距离(纬度1, 经度1, 纬度2, 经度2) {
var R = 6371000
var dLat = (纬度2 - 纬度1) * Math.pi / 180
var dLon = (经度2 - 经度1) * Math.pi / 180
var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(纬度1 * Math.pi / 180) * Math.cos(纬度2 * Math.pi / 180) * Math.sin(dLon/2) * Math.sin(dLon/2)
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
return R * c
}
方案二:二维码签到
原理
管理员生成每日动态二维码,员工扫码签到。二维码内容包含日期和随机验证码,防止截图作弊。
生成签到二维码
已有QRCodeGenerator扩展(extensions/QRCodeGenerator.md)。
when 按钮_生成签到码.Click() {
global 今天 = 计时器1.FormatDateTime(计时器1.Now(), "yyyyMMdd")
global 随机码 = formatAsHex(randomInteger(100000, 999999))
global 签到码 = join("ATTENDANCE:", 今天, ":", 随机码)
QRCodeGenerator1.GenerateQRCode(签到码, 400)
}
扫码签到
使用BarcodeScanner组件:
when 按钮_扫码签到.Click() {
条码扫描器1.DoScan()
}
when 条码扫描器1.AfterScan(结果, 格式) {
if startsWith(结果, "ATTENDANCE:") {
global 部分 = split(结果, ":")
global 日期部分 = selectListItem(部分, 2)
global 今天 = 计时器1.FormatDateTime(计时器1.Now(), "yyyyMMdd")
if 日期部分 == 今天 {
// 验证通过
global 记录 = makeDict(
pair("date", 今天),
pair("time", 计时器1.FormatDateTime(计时器1.Now(), "HH:mm:ss")),
pair("code", selectListItem(部分, 3))
)
保存记录(记录)
标签_状态.Text = "✅ 签到成功!"
} else {
标签_状态.Text = "❌ 二维码已过期"
}
} else {
标签_状态.Text = "❌ 无效的签到二维码"
}
}
防作弊机制
1. 位置模拟检测
when 位置传感器1.LocationChanged(纬度, 经度, 海拔, 速度) {
// 检测GPS模拟
if 位置传感器1.AvailableProviders == "gps" and 位置传感器1.AvailableProviders == "fake" {
标签_状态.Text = "⚠️ 检测到位置模拟"
}
}
2. 动态二维码(每分钟更换)
global 当前验证码 = ""
when Clock_验证码.Timer() {
global 分钟 = 计时器1.Minute(计时器1.Now())
global 小时 = 计时器1.Hour(计时器1.Now())
global 种子 = 小时 * 60 + 分钟
当前验证码 = join("ATT:", 计时器1.FormatDateTime(计时器1.Now(), "yyyyMMdd"), ":", formatAsHex(种子 * 37 + 42))
}
3. WiFi辅助验证
when 按钮_打卡.Click() {
global 连接WiFi = 获取当前WiFi名称() // 需要扩展
if 连接WiFi == "Company_WiFi" {
// WiFi验证通过,允许打卡
} else {
标签_状态.Text = "❌ 请连接公司WiFi"
}
}
数据管理
本地存储结构
TinyDB键: "attendance_records"
值: [
{"date": "2026-05-18", "time": "08:55:30", "type": "签到", "lat": 39.904, "lng": 116.407},
{"date": "2026-05-18", "time": "18:05:12", "type": "签退", "lat": 39.904, "lng": 116.407},
{"date": "2026-05-17", "time": "08:58:45", "type": "签到", "lat": 39.904, "lng": 116.407}
]
上传到服务器
已有api_communication.md和upload_file.md的Web组件用法,此处整合:
procedure 上传记录(记录) {
global JSON = Web客户端1.JsonTextEncode(记录)
global 请求头 = [
"Content-Type: application/json",
join("Authorization: Bearer ", 用户令牌)
]
Web客户端1.RequestHeaders = 请求头
Web客户端1.PostText("https://api.example.com/api/attendance", JSON, "UTF-8")
}
查看打卡记录
procedure 刷新记录列表() {
global 所有记录 = 微数据库1.GetValue("attendance_records", createEmptyList())
global 显示列表 = createEmptyList()
global 今天 = 计时器1.FormatDateTime(计时器1.Now(), "yyyy-MM-dd")
global i = lengthOfList(所有记录)
while i >= 1 { // 倒序显示
global 记录 = selectListItem(所有记录, i)
if dictLookup(记录, "date", "") == 今天 {
global 时间 = dictLookup(记录, "time", "")
global 类型 = dictLookup(记录, "type", "")
addItemsToList(显示列表, join(类型, " ", 时间))
}
i = i - 1
}
ListView_记录.Elements = 显示列表
}
与已有文档的关系
本文整合了以下已有文档中的技术方案,此处只补充考勤场景的具体整合用法:
| 技术 | 已有文档 | 本文用途 |
|---|---|---|
| GPS定位 | sensors.md |
定位打卡 |
| 距离计算 | map_geofence.md |
判断是否在范围内 |
| 二维码 | extensions/QRCodeGenerator.md |
二维码签到 |
| Web API | api_communication.md |
上传打卡记录 |
| TinyDB | db.md |
本地记录存储 |
| 加密 | text_encryption.md |
防作弊验证码 |
| 登录 | login_register.md |
员工身份认证 |
| 时间处理 | timestamp_clock.md |
打卡时间 |
常见问题
Q1: GPS定位精度不够?
- 增大打卡范围(如200-500米)
- 结合WiFi验证
- 使用高精度定位模式
- LocationSensor的
ProviderName设为 “gps”
Q2: 室内GPS信号弱?
- 结合WiFi辅助判断
- 使用蓝牙信标(需要扩展)
- 允许手动输入+管理员确认
Q3: 如何防止代打卡?
- 人脸识别(需要AI扩展)
- 动态二维码+短时效
- 设备指纹(需扩展)
- WiFi+GPS双重验证
总结
| 方案 | 安全性 | 成本 | 适用场景 |
|---|---|---|---|
| GPS打卡 | ⭐⭐ | 低 | 室外/大范围 |
| 二维码签到 | ⭐⭐⭐ | 低 | 室内教室 |
| WiFi打卡 | ⭐⭐⭐ | 低 | 办公室 |
| 蓝牙打卡 | ⭐⭐⭐⭐ | 中 | 精确场景 |
扫码添加客服咨询