羽毛球场预约脚本编写历程🏸

学校的羽毛球场预约系统本来是可以凭手速抢到场地的,但是有一天突然不行了,直接预约系统会卡住然后预约失败,为了保证我和球友们愉快的羽球时光,开发一个自用的预约脚本迫在眉睫

这个博客主要想记录分享一下我的脚本编写经历,并寻求一些改进的意见!

第一步:实现方案的选择

我们的预约系统是“钉钉企业内应用”,本质上是运行在钉钉 WebView 中的 Web 应用。 写预约脚本可以有这些方案:

  • 直接调用预约 API:最快最直接,直接构造 fetch/axios/curl 请求。缺点是需要登录态、业务逻辑和接口参数要拿到。 对于我的情况,有可能实现✅
  • 浏览器自动化:用 Playwright / Puppeteer / Selenium 打开真实网页,自动登录、选日期、选场地、点击预约。 我们预约系统是钉钉里的H5应用,浏览器打开链接用不了,排除❌
  • 屏幕/鼠标键盘自动化:例如 PyAutoGUI、AppleScript、macOS Accessibility API,或者图像识别找到按钮后点击。 感觉还没有手抢快,太慢了,排除❌
  • 页面内 JavaScript 自动化:依旧需要网页形式,我们的钉钉沙盒容器用不了❌

【初步确定直接调用API的方案,接下来试试能不能拿到接口参数】

第二步:接口逆向

先补充一下预约基本信息:我们是每天晚上20:00开放预约隔天的场地,一人一次最多预约2个时间段,一般开放预约后的1~2秒场地被抢空。

我用Charles进行网络抓包,然后电脑钉钉中进行了一次实际预约操作,也是成功拿到了接口信息。 下面是几个重要的接口信息:

  1. 登陆接口:钉钉内打开应用会触发自动登录,向服务器自动发送一个钉钉账号的不知道哪来的ID,可以标识钉钉用户身份。然后服务器处理之后,返回一个预约系统用的token。这个token解析之后发现有效期是5个小时,也就是说预约时间点开始前5小时内抓到的token才可用。
  2. 场地可用性检查接口预约操作的第一步。传入个人信息、预约的时间段、预约的场地号,服务器会返回该场地该时间段是否可预约,如果有部分时间段可约部分不可约,也会返回不可约的时间段信息。
  3. 创建订单接口(/creat_book_info):预约操作的第二步。传入个人信息、预约的时间段、预约的场地号,服务器返回预约成功信息或者失败信息
  4. 服务器时间接口(/creat_order):获取服务器当前时间戳,用于精准定时发送请求。 其中预约操作都需要有效的token,否则返回401未授权错误。 IMG_7943

【nice,接口都有了,接下来只要开发一个脚本,在服务器时间20:00直接发送api预约请求,应该就可以约到了,直接跳过学校那个💩的前端页面,肯定会比手抢快得多】

第三步:最小实现+验证

现在开始写第一版并实际运行测试一下。

具体预约逻辑实现

  1. 抓token:先用charles抓到登录时服务器返回的token,后续请求都带token
  2. 添信息:把个人信息、要预约的时间段、抓到的token 作为脚本的输入信息
  3. 等时间:即使本机显示正好是 20:00,本机时钟和预约服务器也可能相差几十到几百毫秒。所以脚本应该调用服务器时间接口,算出本机和服务器的时间误差offset,保证服务器时间刚好到20:00时脚本发出预约请求
  4. 发请求:并行请求指定时间段的所有场地是否可用 --> 第一个可用场地立即创建订单 --> 返回成功/失败结果

核心代码可以简化为:

  function tryBatch(sites: number[]) {        //输入场地号数组,并发进行预约请求
    return new Promise(resolve => {
      let pending = sites.length;
      let won = false;         //共享状态,有一个场地won时,该场地独享craete_order权限
      let resolved = false;
    
      const finish = (result: object | null) => {       //预约结束函数
        if (resolved) return;
        resolved = true;
        resolve(result);
      };
       
      sites.forEach((siteId, index) => {        //给每个场地创建promise链
        Promise.resolve()
          .then(() => checkSite(siteId))
          .then(async result => {
            if (!result.available || won) return;
      
            won = true;
      
            try {
              const order = await createOrder(siteId);
              finish(order);
            } catch {
              won = false;
            }
          })
          .finally(() => {
            pending--;
    
            if (pending === 0 && !resolved) {
              finish(null);
            }
          });
      });
    });
  }

验证

写完代码,第一次尝试就成功了! 189

第四步:优化流程

  • 抓token流程优化:用Charles抓token太费劲,不够方便。于是写了一个双击即可运行的脚本,运行后打开一下钉钉预约系统,即可自动抓取token并复制到系统剪切板。 Pasted image 20260921143745
  • 预约逻辑优化:增加了场地优先级、多时间段尝试预约、部分冲突时自动预约不冲突的时间段、错峰并发减少服务器限流......等等功能。

第五步:让我的朋友们也用上

目前的使用流程是: --> 运行抓token脚本
--> 把新鲜的token和预约信息写入config.json
--> 终端中运行npm run start 或者 node main.js --> 保持电脑联网开机,直到20:00出结果。 这样的流程对于计算机小白来说感觉不太友好,尤其是后面的终端输入命令(懂得都懂~)。

所以为了让我的朋友们也能预约到场地,我做了下面两个方案:

web

Pasted image 20260921151016

  • 朋友的使用流程:
  1. 双击运行抓token脚本,打开钉钉登陆预约系统,拿到token
  2. 打开我开发的网页,填入token和其他信息,点击”开始抢场“
  3. 抢场任务会运行在我的服务器上,所以用户端不需要保持网页打开
  4. 等到晚上20:00,用户查看结果即可
  • 优点:
    • 操作非常简单,无需电脑,手机也可以查看
    • 点完“开始抢场”即可,不需要保持设备开启/联网,任务运行在服务器。
  • 缺点:
    • ==致命缺陷==:如果有3个及以上用户同时使用这个web进行预约,20:00时我的服务器就会向预约系统发送大量的预约请求,导致预约系统直接给我服务器IP限流了(这个预约系统限流很严格,很容易触发限流😅)。所以肯定无法同时满足多人预约的需求。

agent skill

在项目目录中写了个skill,借助【codex的computer use】,让AI全程托管。 朋友的使用流程:

  1. 和codex说:“clone这个仓库:https://github.com/soul-kk/我的项目地址 ,然后帮我预约后天14:00~16:00的场地,我的个人信息是:********
  2. codex会自动启动抓token脚本,然后使用computer use打开钉钉的预约系统,然后拿到token并启动预约程序。全程自动化
  3. 晚上20:00,用户看codex有没有抢到
  • 优点:
    • 操作最简单
    • 成功率高:本地跑预约请求,不会触发限流,大概率成功!
  • 缺点:
    • 用户要有codex、会用codex
    • 运行期间要保证电脑打开并联网,并且不可关闭codex应用。对于经常在校园内跑来跑去的大学生,有点限制。
    • 有时候不稳定,codex干了什么不太透明,不放心。

未能解决的问题

我理想中的情况是:

用户后天想打球,然后直接向”预约工具“输入时间段,然后继续自己的日常,什么都不用管了。到了晚上20:00,推送预约结果,实现场地自由。并且这个”预约工具“要可以支持多人同时预约!

目前现状和理想的差距还挺大,我觉得主要是受限于下面的因素:

  • 登陆态的获取不方便自动化:想预约,预约系统的token必不可少,而这个token和钉钉强绑定,只有在钉钉里打开预约系统,才能有token抓。就算有用户的钉钉账号密码,也无法通过api换到token!这个token是通过钉钉的登陆态拿到的,而”钉钉的登陆态“是无法提前程序化的拿到的。
  • 服务器限流严重:单一IP如果发送请求过多,服务器直接给你锁死,发请求过少又会失去预约竞争力。
  • 想不到还能有什么形式,可以实现我的理想情景。 ==如果读到这里的朋友能想到一些改进办法,还望各位大佬不吝赐教!==

就写到这里,希望能给大家一些启发,欢迎交流!