群报数自动打卡系统实现原理

15

一个基于微信小程序逆向的自动打卡工具,支持多账号、定时执行、邮件通知。

起因

我的辅导员一位非常"负责"的老师,要求我们每天必须在群报数小程序上进行定位打卡。

每天打卡这件事本身不难,难的是每天都记得打卡。忘记一次就要被点名,与其每天提心吊胆怕忘记,

不如花点时间一劳永逸——分析一下群报数小程序的内部结构,写个自动化脚本。

于是就有了这个项目。

核心难点:Token 获取链路

整个系统最关键的部分是如何获取打卡接口的 Authorization Token。这涉及到微信小程序的授权机制。

认证流程图

用户微信授权
     ↓
获取 refresh_token(长期有效,约30天)
     ↓
调用微信 API 刷新 → 获取 access_token(短期有效,2小时)
     ↓
用 access_token 登录群报数 → 获取 Authorization Token
     ↓
携带 Authorization 调用打卡接口

第一步:抓取初始 Token

工具准备

  • 安卓手机 + 微信
  • 抓包工具(HttpCanary / Charles / Fiddler)
  • 需要配置 HTTPS 证书信任

抓取目标

打开群报数小程序,抓取微信授权请求,找到以下关键信息:

{
  "access_token": "98_xxxxx",
  "refresh_token": "98_xxxxx",  // 这个最重要!
  "openid": "oxkDB6xxxxx",
  "unionid": "oBFnS5xxxxx"
}

关键点refresh_token 是长期有效的(约30天),只要定期使用就会自动续期。这是整个自动化的基础。

第二步:刷新 access_token

微信提供了 refresh_token 刷新接口:

def refresh_wechat_token(refresh_token):
    url = "https://api.weixin.qq.com/sns/oauth2/refresh_token"
    params = {
        "appid": "wxe7be3420358ab61e",  # 群报数小程序的 appid
        "grant_type": "refresh_token",
        "refresh_token": refresh_token
    }
    
    response = requests.get(url, params=params)
    result = response.json()
    
    # 返回新的 access_token 和 refresh_token
    return {
        "access_token": result["access_token"],
        "refresh_token": result["refresh_token"],  # 新的 refresh_token,需要保存
        "openid": result["openid"]
    }

注意:每次刷新会返回新的 refresh_token,必须保存更新,否则旧的会失效!

第三步:登录群报数获取 Authorization

拿到 access_token 后,调用群报数的登录接口:

def login_qun100(openid, unionid, access_token):
    url = "https://form.qun100.com/v1/app/login"
    
    payload = {
        "appId": "wxe7be3420358ab61e",
        "openId": openid,
        "sessionKey": access_token,  # 这里用 access_token 作为 sessionKey
        "unionId": unionid
    }
    
    headers = {
        'Content-Type': 'application/json; charset=utf-8',
        'User-Agent': 'okhttp/4.11.0'
    }
    
    response = requests.post(url, json=payload, headers=headers)
    result = response.json()
    
    # 返回 Authorization Token
    return result["data"]["token"]

第四步:调用打卡接口

有了 Authorization,就可以调用打卡接口了:

def do_checkin(authorization, name, location):
    url = "https://form.qun100.com/v1/1767814617359114241/form_data"
    
    headers = {
        'Authorization': authorization,
        'Content-Type': 'application/json',
        'Client-App-Id': 'wx6b67694378f555b6',
        'Client-Form-Id': '1767814617359114241',
        'User-Agent': 'Mozilla/5.0 ... MicroMessenger/8.0.65 ...',
        'Referer': 'https://servicewechat.com/wx6b67694378f555b6/173/page-frame.html'
    }
    
    payload = {
        "catalogs": [
            {
                "cid": "1767814618705485825",
                "type": "WORD",
                "value": name  # 姓名
            },
            {
                "cid": "1767814618709680129",
                "type": "LOCATION",
                "value": {
                    "address": location["address"],
                    "title": location["title"],
                    "location": {
                        "coordinates": location["coordinates"],
                        "type": "Point"
                    }
                }
            }
        ],
        "formVersion": 5
    }
    
    response = requests.post(url, json=payload, headers=headers)
    return response.json()

完整的 Token 刷新链路

def get_valid_authorization(account):
    """获取有效的 Authorization Token"""
    
    # 1. 用 refresh_token 刷新微信 token
    wx_result = refresh_wechat_token(account["refresh_token"])
    if not wx_result:
        return None
    
    # 2. 保存新的 refresh_token(重要!)
    account["refresh_token"] = wx_result["refresh_token"]
    save_config()
    
    # 3. 登录群报数获取 Authorization
    authorization = login_qun100(
        account["openid"],
        account["unionid"],
        wx_result["access_token"]
    )
    
    return authorization

定时任务设计

为了模拟真人操作,打卡时间在指定时间段内随机:

def schedule_next_checkin(account):
    start_hour = 18  # 开始时间
    end_hour = 19    # 结束时间
    
    # 随机分钟和秒
    random_minute = random.randint(0, 59)
    random_second = random.randint(0, 59)
    
    # 如果今天已打卡,安排明天
    if is_today_checked_in(account_id):
        next_time = tomorrow.replace(hour=start_hour, minute=random_minute)
    else:
        next_time = today.replace(hour=start_hour, minute=random_minute)
    
    return next_time

配置文件结构

{
  "wechat_app": {
    "appid": "wxe7be3420358ab61e"
  },
  "accounts": [
    {
      "id": "用户ID",
      "name": "真实姓名",
      "wechat": {
        "openid": "oxkDB6xxxxx",
        "unionid": "oBFnS5xxxxx",
        "refresh_token": "98_xxxxx"
      },
      "location": {
        "address": "完整地址",
        "title": "地点名称",
        "coordinates": [经度, 纬度]
      },
      "schedule": {
        "start_hour": 18,
        "end_hour": 19
      },
      "email_receivers": ["xxx@qq.com"]
    }
  ]
}

关键技术点总结

技术点 说明
refresh_token 续期 每次使用后保存新的 token,保持长期有效
请求头伪装 User-Agent、Referer 等必须模拟微信环境
随机时间 避免固定时间打卡被检测
错误处理 token 过期时邮件通知,需要重新抓包

技术栈

  • 后端:Python + Flask
  • 数据库:SQLite
  • 前端:TailwindCSS + 腾讯地图
  • 通知:SMTP 邮件

本文仅供技术学习交流,请勿用于违规用途。