Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
badminton
badminton.exe
.gocache
275 changes: 269 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,272 @@
# 北理工羽毛球抢票

## 快速使用
一个用于北理工体育馆羽毛球场地预约的命令行小工具。程序会根据你输入的场馆、日期、场地和时间段持续检查目标时段,并在可下单时尝试提交订单;检测到未支付订单后,可选发送钉钉机器人通知。

1. 使用抓包工具抓取 http://gym.dazuiwl.cn 下的任意请求(触发方式为使用微信打开北理工体育馆公众号的预约功能,可以随便操作一些),找到请求参数中的`token`、`openid`填入/config/config.go的对应位置,这里的openid对每一个微信号唯一(微信分发的),确保不要泄露。
2. 运行`go build main.go`编译
3. 无运行时参数,运行后先输入25-良乡,51-中关村,54-中关村早晚场中的一个来确定场馆;然后输入YYYY-MM-DD格式的日期选择预定日期,例如2026-12-13;之后控制台会打印对应日期对应场馆的预定情况,接下来选择场地和时间:如要选择主馆4,09:00~10:00,就依次输入“主馆4”,“9”。
4. 程序在07:00~23:30以内的时间会尝试立刻下单;在此阶段以外的时间会等待至第二天早七点立刻下单
5. 理论上适用于其他类型的场地,可以自行在运行时输入不同的场馆id来测试。
## 使用提醒

- 本项目仅建议本人账号自用。
- 不要泄露 `token`、`openid`、钉钉 `Webhook` 配置,这些信息可以代表你的账号或通知机器人发起请求。
- 不要把填写了真实配置的 `config/config.go` 上传到公开仓库。
- 程序不能保证一定抢到场地,最终结果以体育馆预约系统实际订单为准。

## 准备工作

建议优先使用以下环境:

- Windows
- WSL2 Debian

项目只在 Windows 和 WSL2 Debian 上做过实际测试。Go 官方提供 Windows、macOS、Linux 的安装包,所以 Linux/macOS 理论上也可以运行;主要差别通常只是编译输出文件名是否带 `.exe`。

你需要准备:

- Go 环境:[Go 官方下载页](https://go.dev/dl/)
- 抓包工具,例如 Reqable
- 微信,登录你自己的账号
- 钉钉自定义机器人,可选,用于抢到后通知,防止抢到后错过 15 分钟支付有效期

## 抓取 token 与 openid

下面以 Reqable 为例。

1. 打开 Reqable,开始抓包。
2. 使用微信打开“北理工体育馆”公众号的预约功能。
3. 在预约页面随便进行一些操作,例如查看场馆、日期、时段、提交预约的订单信息。
4. 在 Reqable 中筛选:

```text
http://gym.dazuiwl.cn
```

5. 找到请求里的 `token` 和 `openid`:
- `token`:通常大多数接口的请求头里都能看到,可以找一找。
- `openid`:通常在点击“提交订单-提交并支付订单”后相关接口(例如 `/api/order/submit`)的请求体里可以找到,不需要真支付。

目前公众号预约接口是 HTTP 请求,通常可以直接查看内容,不需要安装 CA 证书,只有抓 HTTPS 请求时,才涉及安装 CA 证书和信任证书。

## 安装 Go 环境

安装 Go 可参考 Go 官方下载页,安装完成后在终端验证:

```powershell
go version
```

`go.mod` 中写的是:

```text
go 1.23
```

使用 Go 1.23 或更新版本均可尝试,本地测试使用 Go 1.26.3 版本也能运行。

## 获取项目源码

方式一:使用 Git 克隆。

```powershell
git clone <仓库地址>
cd BitBMTGrab
```

方式二:下载 ZIP,解压后进入项目目录。

```powershell
cd <解压后的项目目录>
```

## 配置账号信息

打开 [config/config.go](config/config.go),填写:

```go
const ApiToken = "替换为你的token"
const OpenId = "替换为你的openid"
```

示例:

```go
const ApiToken = "xxxxxxxx"
const OpenId = "xxxxxxxx"
```

这些配置不要泄露。

## 配置钉钉通知(可选)

钉钉自定义机器人可以通过 Webhook 向群里发送消息。参考:[钉钉开发者百科:创建自定义机器人](https://open.dingtalk.com/document/dingstart/custom-bot-creation-and-installation)。

配置步骤:

1. 在钉钉群里添加“自定义机器人”。
2. 安全设置选择“自定义关键词”。
3. 关键词建议设置为:

```text
抢票通知
```

4. 复制机器人 Webhook URL,填写到 [config/config.go](config/config.go):

```go
const DingTalkWebhook = "https://oapi.dingtalk.com/robot/send?access_token=xxx"
```

如果不需要钉钉通知,保持空字符串即可:

```go
const DingTalkWebhook = ""
```

测试钉钉通知:

```powershell
go run . --testdingtalk
```

如果配置正确,钉钉群会收到一条测试消息。默认通知标题包含“抢票通知”,用于匹配钉钉机器人关键词策略。

## 运行程序

直接运行:

```powershell
go run .
```

编译后运行。

Windows:

```powershell
go build -o badminton.exe .
.\badminton.exe
```

WSL2 Debian / Linux / macOS:

```bash
go build -o badminton .
./badminton
```

输出文件名可以自己改,Windows 通常使用 `.exe` 后缀,Linux/macOS/WSL 通常不需要后缀。

## 输入预约参数

程序启动后会依次要求输入:

1. 场馆 ID
2. 预约日期
3. 场地名称
4. 预约开始小时

场馆 ID:

```text
25 - 良乡
51 - 中关村
54 - 中关村早晚场
```

示例输入:

```text
请输入场馆ID(25-良乡,51-中关村,54-中关村早晚场): 25
请输入预订日期(格式:YYYY-MM-DD): 2026-05-11
请输入场地名称: 场4
请输入预订时间(小时,例如9表示09:00开始的时段): 17
```

其中:

- 日期必须是 `YYYY-MM-DD` 格式。
- 场地名称必须和程序打印出来的场地名称一致。
- 时间只输入开始小时,例如 `17` 表示 `17:00` 开始的时段。

## 运行状态说明

程序会先打印目标日期的场地预订情况,然后进入检查和下单流程。

常见输出含义:

```text
检查订单失败: 订单检查失败: 场地该时间段预约中,5秒后继续检查...
```

目标时段当前不可下单,但属于可等待状态。程序会每 5 秒继续检查。

```text
检查通过,准备进入下单流程...
```

`CheckSportSchedule` 检查通过,程序准备尝试提交订单。

```text
提交订单失败: xxx,5秒后回到检查阶段...
```

提交接口失败,程序不会继续硬提交,而是等待 5 秒后重新回到检查阶段。

```text
订单提交成功,开始确认订单...
```

提交接口返回成功,程序开始查询未支付订单列表。

```text
已检测到订单 123,正在等待支付...
```

程序检测到未支付订单,说明已经抢到待支付订单,此时如果配置了钉钉 Webhook,会发送钉钉通知。

建议每次使用时先观察一轮检查输出,因为程序目前只对“预约中”状态持续等待;如果接口返回的是其他状态,例如:

```text
场地该时间段上课中
场地该时间段包场中
```

程序会认为这不是可恢复的临时状态,并直接退出。

程序在 `07:00:00` 到 `23:30:00` 之间会尝试立刻下单;如果不在这个时间范围内,会等待到下一个早上 7 点再继续。

## 常见问题

### `go` 命令找不到

说明 Go 没安装好,或者环境变量没有配置好。重新安装 Go 后,打开一个新的终端执行:

```powershell
go version
```

### `token` 或 `openid` 失效

重新抓包并更新 [config/config.go](config/config.go) 中的 `ApiToken` 和 `OpenId`。

### 场地名称不匹配

程序按场地名称精确匹配。请使用程序打印出来的场地名称,不要自己猜名称。

### 钉钉通知不触发

检查:

- `DingTalkWebhook` 是否为空。
- Webhook URL 是否完整。
- 钉钉机器人是否启用了关键词安全策略。
- 钉钉机器人配置的关键词是否能匹配通知标题里的“抢票通知”。

可以先运行:

```powershell
go run . --testdingtalk
```

如果仍然失败,根据控制台打印的钉钉错误信息继续排查,也可以把错误信息发给 AI 辅助分析。

### 程序一直没有抢到

可能是目标时段一直没有释放、网络较慢、接口限制、账号状态异常或场地不可预约,程序只能自动执行检查和提交,不能绕过预约系统规则。
67 changes: 57 additions & 10 deletions badminton.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"badminton/api"
"badminton/models"
"fmt"
"os"
"sort"
"strings"
"time"
)

Expand Down Expand Up @@ -124,6 +126,15 @@ func waitUntilNextAvailableTime() {
}

func main() {
if len(os.Args) > 1 && os.Args[1] == "--testdingtalk" {
if err := sendDingTalkTestNotice(); err != nil {
fmt.Printf("钉钉通知测试失败: %v\n", err)
return
}
fmt.Println("钉钉通知测试发送成功")
return
}

headers := map[string]string{
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
}
Expand Down Expand Up @@ -190,16 +201,23 @@ func main() {

scene := fmt.Sprintf("[{\"day\":\"%s\",\"fields\":{\"%s\":[%d]}}]", dateStr, venueFieldID, hourID)

check, err := api.CheckSportSchedule(venueID, scene, headers)
if err != nil {
fmt.Printf("检查订单失败: %v\n", err)
return
}
for {
check, err := api.CheckSportSchedule(venueID, scene, headers)
if err != nil {
if strings.Contains(err.Error(), "预约中") {
fmt.Printf("检查订单失败: %v,5秒后继续检查...\n", err)
time.Sleep(5 * time.Second)
continue
}

fmt.Println("\n等待可下单时间...")
waitUntilNextAvailableTime()
fmt.Printf("检查订单失败: %v\n", err)
return
}

fmt.Println("\n检查通过,准备进入下单流程...")
fmt.Println("\n等待可下单时间...")
waitUntilNextAvailableTime()

for {
orderList, err := api.GetOrderList("makeappointment", "created", "", 1, 20, headers)
if err != nil {
fmt.Printf("获取订单列表失败: %v\n", err)
Expand All @@ -208,16 +226,45 @@ func main() {

for _, order := range orderList.Data.List {
if order.SportEventsID == venueID {
err = sendOrderNotice(venueID, venueName, dateStr, hour, order.ID, order.Amount)
if err != nil {
fmt.Printf("钉钉通知失败: %v\n", err)
}
fmt.Printf("已检测到订单 %d,正在等待支付...\n", order.ID)
return
}
}

_, err = api.SubmitOrder(venueID, check.Data.TotalAmount, scene, headers)
if err != nil {
fmt.Printf("提交订单失败: %v\n", err)
fmt.Printf("提交订单失败: %v,5秒后回到检查阶段...\n", err)
time.Sleep(5 * time.Second)
continue
}

fmt.Println("订单提交成功,开始确认订单...")
for {
orderList, err := api.GetOrderList("makeappointment", "created", "", 1, 20, headers)
if err != nil {
fmt.Printf("获取订单列表失败: %v,5秒后继续确认...\n", err)
time.Sleep(5 * time.Second)
continue
}

for _, order := range orderList.Data.List {
if order.SportEventsID == venueID {
err = sendOrderNotice(venueID, venueName, dateStr, hour, order.ID, order.Amount)
if err != nil {
fmt.Printf("钉钉通知失败: %v\n", err)
}
fmt.Printf("已检测到订单 %d,正在等待支付...\n", order.ID)
return
}
}

fmt.Println("暂未检测到未支付订单,5秒后继续确认...")
time.Sleep(5 * time.Second)
}
time.Sleep(5 * time.Second)
}

}
6 changes: 6 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ package config
const ApiHost = "http://gym.dazuiwl.cn"
const ApiToken = "替换为你的token"
const OpenId = "替换为你的openid"

// DingTalkWebhook 留空表示禁用钉钉通知。
// 启用时填写完整机器人 Webhook,例如:https://oapi.dingtalk.com/robot/send?access_token=xxx
// 钉钉机器人安全设置请选择“自定义关键词”,请确保给机器人配置的关键词包含在通知标题中;
// 默认通知标题包含“抢票通知”。
const DingTalkWebhook = ""
Loading