# 羽毛球分組配對器 這是一個使用 `React + Vite + TypeScript` 製作的網頁應用程式,用來排羽毛球場地賽程與隊伍配對。 - 首頁(`/`)是 V2:2、3 號場地各排 8 局雙打 - `/v1` 保留舊版:A / B 區三輪配對 ## V2 功能說明(首頁) - 輸入或用 `讀取出席名單` 載入出席成員(來源同 V1 的 attendance API) - 每局從全部成員中挑 8 人上場(2、3 號場地各 4 人打雙打),其他人休息 - 成員不固定場地,每局重新分配;系統會讓每個人上場局數盡量平均(落差最多 1 局) - 搭檔組合在全部組合輪完一遍之前不會重複;對手組合也會盡量錯開 - 頁面載入時會先透過 `GET /api/skills` 抓 `tg_members` 的實力表,載入完成前不能產生賽程 - 依實力權重排點:在不重複的前提下,搭檔盡量強弱互補、每局兩隊實力總和盡量接近(查不到的人權重當 1);對戰表平常只顯示名字,滑鼠停在隊伍上會浮出實力泡泡(例如 `阿志 1、蓉蓉 4(合計 5)`) - 人數 4~7 人時只排 2 號場地 8 局;未滿 4 人無法排賽程 - `讀取指定日期` 可把之前上傳的 V2 賽程從資料庫回填到畫面;該日還沒賽程時會自動改載入出席名單(V1 格式的舊資料請到 `/v1` 讀取) - 上傳沿用原本的 `badminton` 表:`personnel` 存全部出席者、`battlecombination` 的 `"0"` / `"1"` 分別存 2、3 號場地的 16 組搭檔(依序每 2 組為一局的對戰雙方) - `推送到 LINE` 跟著 `LINE_TARGET_MODE` 走:NAS 正式環境(`prod`)推到羽球群、本機開發(`local`)推到測試對話;一樣要按兩次確認,`local` 模式時按鈕會標示「測試」 ## V1 功能說明(`/v1`) - 分別輸入 A 區與 B 區名單 - 可指定要寫入資料庫的目標日期 - 每次固定產生 3 組完整配對結果 - 每一隊都由 `1 位 A 區 + 1 位 B 區` 組成 - 每一組都會把 A 區與 B 區名單完整分配完成 - 兩區人數差 2 以上時,每輪會輪流讓人多的一區成員跨區支援 - 跨區平衡後仍有缺口(總人數為奇數)時,才補入「那個」 - 產生配對後可用按鈕手動上傳資料到資料庫 - 若指定日期已經有資料,上傳前會先詢問是否覆蓋 - 可讀取指定日期的資料庫內容並回填到畫面 - 若指定日期還沒有配對資料,`讀取指定日期` 會自動改抓 Telegram bot 的出席名單 - 也可直接按 `讀取今日出席` 帶入出席名單 - 載入出席名單時會依 `tg_members.skill` 實力權重自動分區:實力強的一半放 A 區、弱的一半放 B 區(可再手動調整) - 兩邊都沒有資料時,畫面會顯示對應的錯誤訊息 - 組隊結果產生後可一鍵主動推播到指定 LINE 對話(需按兩次確認) - 支援換行、半形逗號、全形逗號與頓號輸入 - 會自動去除空白與重複名稱 ## V1 操作流程 1. 選擇 `目標日期` 2. 按 `讀取今日出席` 帶入 Telegram 報名名單(或手動輸入 A 區與 B 區名單) 3. 系統會依實力權重自動分好 A / B 區,需要時再手動微調 4. 按下 `產生三組配對` 5. 按下 `上傳資料` 將資料寫入 DB 6. 若需要,可按 `讀取指定日期` 載回既有資料;該日還沒配對時會自動改載入出席名單 7. 組隊結果顯示後,可按 `推送到 LINE`(按兩次確認才會送出) ## 讀取今日出席 出席名單來自 Node-RED 的 Telegram bot「建喵打羽球」:群組成員按「參加」後會寫進同一個資料庫的 `attendance` 表,按「不參加」則直接刪除,所以表裡的就是當天出席名單。 - 使用現有的 `DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_DATABASE` 連線,不需要新的環境變數 - API:`GET /api/attendance/:time`(`time` 為 `YYYYMMDD`),回傳 `{ ok: true, data: { time, names: [...], members: [{ name, skill }] } }`;沒資料回 404 - 每個人的 `skill` 來自 `tg_members.skill`(優先用 `user_id` 對應,對不到再用 `nickname` 對應),查不到就當 1 - 載入後依 `skill` 由高到低排序,前一半(無條件進位)放 A 區、後一半放 B 區;同分時維持報名順序 - 載入時會沿用名單去重邏輯,並清空既有配對結果 `attendance` 表結構: ```sql CREATE TABLE attendance ( poll_date INT(11) NOT NULL COMMENT '場次日期 YYYYMMDD', user_id BIGINT NOT NULL COMMENT 'Telegram user id', nickname VARCHAR(64) NOT NULL COMMENT '顯示名稱(就是要放進 personnel 的名字)', tg_name VARCHAR(128) NULL COMMENT 'Telegram 顯示名', joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '報名時間', PRIMARY KEY (poll_date, user_id) ) CHARSET utf8mb4; ``` ### 實力權重 `tg_members` 表加了 `skill` 欄位(`TINYINT UNSIGNED NOT NULL DEFAULT 1`,範圍 1~10,10 最強),這個專案只會讀取、不會寫入: ```sql UPDATE tg_members SET skill = 8 WHERE nickname = '柏威'; ``` bot 另外還有 `tg_poll`(報名訊息 id)表,這個專案不會讀寫;`tg_members` 也只讀 `skill` 與 `nickname` 對應,不會修改。 ## 使用方式 ```bash npm install npm run dev ``` 前端開發網址預設為: ```text http://localhost:3500 ``` 推播到 LINE 時,會直接使用 Flex Message 主動送到指定對話,格式參考既有 `line-bot-ts` 羽球查詢結果樣式。 若指定日期尚未上傳資料,系統會先提示你上傳後再推播。 LINE 推播目標支援分成兩組: - `LINE_TARGET_ID_LOCAL`: 本地測試用對話 - `LINE_TARGET_ID_PROD`: 正式環境用對話 - `LINE_TARGET_MODE`: `local` 或 `prod` 當系統偵測目前為 `local` 模式時,頁面右上角會顯示「測試環境」標籤。 正式環境 `prod` 不會顯示這個標籤。 ## 資料庫欄位 - `time`: 目標日期,格式為 `YYYYMMDD` - `personnel`: 人員清單,格式為 `[[1,"A區成員"],[0,"B區成員"]]` - `battlecombination`: 三組隊伍搭配,格式為 `{"0":[["A","B"]],"1":[...],"2":[...]}` ## 正式建置 ```bash npm run build ``` ## Docker 部署 這個專案已提供單容器部署方式,容器啟動後會同時提供: - 網頁前端 - `/api` 後端 - MariaDB 寫入功能 ### 建立映像 ```bash docker build -t badminton-match-hub . ``` ### 啟動容器 ```bash docker run -d \ --name badminton-match-hub \ -p 3500:3500 \ -e PORT=3500 \ -e DB_HOST=192.168.0.15 \ -e DB_PORT=3307 \ -e DB_USER=jianmiau \ -e DB_PASSWORD=你的密碼 \ -e DB_DATABASE=badminton \ -e DB_TABLE=badminton \ badminton-match-hub ``` ### NAS 上建議設定 - 容器埠使用 `3500` - 對外埠可自訂,例如 `3500:3500` - 環境變數請在 NAS 的 Docker 介面中填入 - 不要把本機 `.env` 直接打包進映像 部署完成後可直接透過: ```text http://NAS_IP:3500 ``` 開啟系統。 ### docker-compose 專案也已提供 `docker-compose.yml`。 ```bash docker compose up -d --build ``` Compose 版本預設會: - 對外使用 `3500` - 容器內部使用 `3500` - 讀取 `.env` 內的資料庫設定 - 預設使用 `LINE_TARGET_MODE=prod` - 推播到 `LINE_TARGET_ID_PROD` 也就是說,在 NAS 上直接執行: ```bash sudo docker compose up -d --build ``` 就會以正式環境方式啟動。 請先確認 `.env` 至少有以下設定: ```env LINE_CHANNEL_ACCESS_TOKEN=你的token LINE_TARGET_ID_LOCAL=你的測試對話ID LINE_TARGET_ID_PROD=你的正式對話ID ``` 部署後可透過: ```text http://NAS_IP:3500 ``` 開啟系統。