App Inventor 2 考勤系统教程 - 定位打卡与二维码签到

« 返回首页

概述

考勤系统是企业/学校常见需求。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打卡 ⭐⭐⭐ 低 办公室
蓝牙打卡 ⭐⭐⭐⭐ 中 精确场景
文档反馈