根本原因: 原本的「今日羽球報名」bot 寫在 Node-RED 的 function 節點裡,不易測試與擴充; 羽球團又需要在 Telegram 群組內直接記分、看發球者,並把戰績寫進 web 版共用的 DB。 影響: 同一個 bot(@JianMiauBadmintonBot)改由本專案服務:報名指令、按鈕、暱稱、 週一 10:00 排程、白名單行為與訊息格式皆與 Node-RED 版相同,舊訊息按鈕仍可用; 另新增 /記分(/score、/new)記分板,並在結束/再一局時寫入共用的 history 表。 修法: - signup.js:逐句移植 Node-RED bd_sm 狀態機(attendance / tg_poll / tg_members) - scoreboard.js + match.js + render.js:羽球規則(發球區、換位、Deuce、30 分封頂)、 上排 1 2/下排 4 3 版面、從今日出席選人開局、⏱ 結算、兩段式結束確認、 開局者/管理員權限、scoreList(web 版格式) - history.js:寫入 history 表,欄位與 web 版 ensureHistoryTable 相同 - scripts/nodered-switch.mjs:透過 admin API 暫停/恢復 Node-RED 舊流程 - 單元測試 23 項(規則、報名、選人、結算、權限、歷史寫入),Dockerfile / compose Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
112 lines
6.0 KiB
Markdown
112 lines
6.0 KiB
Markdown
# tg-shuttle-score — 建喵打羽球 Bot
|
||
|
||
Telegram bot「建喵打羽球 @JianMiauBadmintonBot」的 Node.js 版本,包含:
|
||
|
||
1. **今日羽球報名**(從 Node-RED「羽球報名」分頁移植,行為、訊息格式、DB 表完全相同)
|
||
2. **群組記分板**(新功能):`/記分` 開局,訊息本身就是記分板,用 inline 按鈕記分
|
||
|
||
只在白名單群組運作:`建喵測試 (-1002488036686)`、`羽球團~ (-4679836160)`。
|
||
|
||
## 指令
|
||
|
||
| 指令 | 英文別名 | 說明 |
|
||
| --- | --- | --- |
|
||
| `/羽球 [YYYYMMDD]` | `/badminton` | 發今日(或指定日期)報名訊息,附「✅ 參加 / ❌ 不參加」按鈕 |
|
||
| `/羽球名單` | `/bdlist` | 回今天的參加名單 |
|
||
| `/羽球暱稱 名字` | `/bdname` | 設定報名顯示的名字(存 `tg_members`,綁 user_id) |
|
||
| `/記分` | `/score`、`/new` | 列出今天出席的人(`attendance`),依 1→4 點選上場人員:1、2 一隊,3、4 一隊;點滿 4 人自動開局,選 2 人可按「單打開始」 |
|
||
| `/記分 小明 小華 vs 阿強 阿美` | `/score`、`/new` | 直接開一場雙打記分板(單打各 1 人;最後加數字可改目標分) |
|
||
| `/help` | | 指令說明 |
|
||
|
||
英文指令已用 `setMyCommands` 註冊(啟動時自動),群組打 `/` 會有選單。
|
||
|
||
### 報名按鈕
|
||
|
||
- 參加 → `INSERT attendance`;不參加 → `DELETE`;重按只跳提示、不動 DB
|
||
- 名單直接改在原訊息上;`callback_data` 為 `bd|yes||YYYYMMDD` / `bd|no||YYYYMMDD`,**Node-RED 時期發的舊訊息按鈕仍可用**
|
||
- 每週一 10:00(`SIGNUP_CRON`)自動發到正式群
|
||
|
||
### 記分板按鈕
|
||
|
||
- **權限**:只有開局者(下 `/記分` 的人)或群組管理員(creator / administrator,查詢結果快取 5 分鐘)能按;其他人按了只跳提示「只有開局者或群組管理員可以操作記分板」。報名按鈕不受限。
|
||
|
||
- 選人(無參數開局):點名字依序標 1️⃣~4️⃣,`↩️ 取消上一位`、`▶️ 單打開始`(剛好 2 人時)、`✖️ 取消`
|
||
- 開局:`🅰️ / 🅱️ 先發球`;雙打可 `🔀 換位` 決定誰站右區(誰先發/先接)
|
||
- 進行中(三排):`🅰️ +1` `🅱️ +1` / `↩️ 上一步` `🔄 重新開始` / `⏱ 結算` `🏁 結束`
|
||
- `⏱ 結算`:時間到以目前比分定勝負(領先方 🏆、同分平手),可 `上一步` 復原
|
||
- 分出勝負顯示 🏆;`🏁 結束` 需兩段確認(第一次按 → 出現 `✅ 確定結束 / ↩️ 取消`,按其他按鈕也會取消),確定後移除按鈕、鎖定
|
||
- 規則:偶數分右區發、發球方得分同隊換位續發、失分換發不換位、21 分制、Deuce 領先 2 分、30 分封頂(與 badminton-scoreboard 一致)
|
||
|
||
## 安裝與啟動
|
||
|
||
```bash
|
||
npm install
|
||
cp .env.example .env # 填 BOT_TOKEN、DB_*、ALLOWED_CHAT_IDS
|
||
npm test # 15 個單元測試(規則、報名流程、選人、指令解析)
|
||
npm start # 或 npm run dev
|
||
```
|
||
|
||
### ⚠️ 同一個 bot 只能有一個 polling 端
|
||
|
||
Node-RED(`http://192.168.5.45:1880`)的「羽球報名」分頁已於 2026-08-18 停用,由本專案接手。
|
||
若要暫時切回 Node-RED,或反過來,用下列指令(兩邊同時 poll 會互相 409):
|
||
|
||
```bash
|
||
npm run nodered:status # 看目前狀態
|
||
npm run nodered:pause # 停用 bd_tab + bot 設定改 send-only → 之後 npm start
|
||
npm run nodered:resume # 還原 Node-RED(本專案要先停)
|
||
```
|
||
|
||
(透過 Node-RED admin API,deploy type = nodes,只重啟有變動的節點。)
|
||
|
||
## Docker / NAS
|
||
|
||
```bash
|
||
sudo docker compose up -d --build
|
||
```
|
||
|
||
`docker-compose.yml` 讀 `.env`,狀態檔掛在 `./data`。上線步驟:NAS 起容器 → 確認 log 出現「已啟動」→ `npm run nodered:pause`。
|
||
|
||
## 資料庫
|
||
|
||
與 badminton-scoreboard / Node-RED 共用 `jianmiau.tk:3307/badminton`:
|
||
|
||
| 表 | 用途 |
|
||
| --- | --- |
|
||
| `attendance` | 今日出席(只存要參加的人;PK poll_date+user_id) |
|
||
| `history` | 完成的比賽戰績(與 web 版共用,記分板寫入) |
|
||
| `tg_poll` | 每天報名訊息的 chat_id / message_id(改暱稱後回頭更新用) |
|
||
| `tg_members` | `/羽球暱稱` 設的名字,綁 user_id |
|
||
|
||
記分板狀態存 `DATA_FILE`(JSON),重啟後舊訊息按鈕仍可用。
|
||
|
||
**歷史戰績**:分出勝負(打到目標分或 `⏱ 結算`)且有得分的比賽,在按 `✅ 確定結束` 或 `🔄 再一局` 時寫入 `history` 表(`DB_HISTORY_TABLE`,預設 `history`,欄位與 web 版相同),web 版「歷史戰績」頁直接看得到。未結算就結束的比賽不會存(確認提示會寫明)。寫入失敗時不會結束,可再按一次重試。
|
||
- `players = [1號,2號,3號,4號]`、`team = [[1,2],[3,4]]`、`score = [🅰️,🅱️]`、`type` 0 雙打/1 單打、`winScore` 目標分、`dayOfWeek` 台北時區
|
||
- 單打 `players` 存 `[p1,p1,p3,p3]`(web 歷史頁用 `players[starter]` 顯示發球者)
|
||
|
||
每一分都會累積 `scoreList`(不顯示在 TG,供之後寫 `history` 用),格式沿用 web 版:`[round, starter, winCount, winner]`
|
||
- `round` 第幾分(0 起算)
|
||
- `starter` 發球位置編號 0~3 = 畫面 1~4 號位減 1(🅰️ 上排:偶數分 0、奇數分 1;🅱️ 下排:偶數分 2、奇數分 3;只看位置不看人)
|
||
- `winCount` 同隊連續得分數,第一分為 0
|
||
- `winner` 得分隊 0 = 🅰️、1 = 🅱️
|
||
- 上一步會一併回退;重新開始清空;結算不影響
|
||
|
||
## 專案結構
|
||
|
||
```
|
||
src/
|
||
index.js 進入點:白名單、指令註冊、callback 分流、排程
|
||
config.js .env 讀取
|
||
db.js mysql2 連線池
|
||
util.js 共用小工具(HTML 跳脫、日期、指令 regex)
|
||
signup.js 報名功能(移植自 Node-RED bd_sm 狀態機)
|
||
scoreboard.js 記分板功能
|
||
match.js 羽球規則(純邏輯)
|
||
render.js 記分板文字 + 鍵盤
|
||
store.js 記分板狀態持久化
|
||
scripts/
|
||
nodered-switch.mjs 暫停 / 恢復 Node-RED 舊流程
|
||
test/
|
||
match.test.js signup.test.js
|
||
```
|