功能:首頁改為 V2 場地排程,載入與手動產生皆為 2 場地各 8 局並依實力配對

摘要:
- 從資料庫讀取 match-hub V2 資料(battlecombination 以場地為 key)時,
  顯示「2 號場地」「3 號場地」各 8 局賽程,挑一個場地帶進記分板
- 手動分組移除 A / B 區,改為「今日出席名單」+「其他區名單」,
  演算法移植 match-hub V2:依 tg_members.skill 實力強弱互補、兩隊平衡,
  搭檔輪完一遍前不重複、每人局數落差最多 1 局;8 人以上排兩場地
- server 新增 GET /api/skills(表名可用 DB_MEMBERS_TABLE 覆寫)
- 選隊面板預設隊伍照局分組並顯示局數標題;修正帶入現有對戰時
  第 2 隊不顯示「已放入」且點擊無法取消的問題(配對比對改為不分順序)
- 舊 V1 三輪資料(personnel 含 B 區)仍照「第 N 組」顯示,自動判別

根本原因:
- 實際打球流程是 2、3 號場地各打 8 局、每局換搭檔輪休,
  舊的三輪配對制不符流程,也完全沒考慮實力;
  選隊面板的隊伍比對寫死正序,帶入現有對戰時右隊兩人反序就對不上

影響:
- 首頁載入與手動產生的結果格式一致(N 號場地 × 8 局),操作流程不變
- 舊版 A / B 區的 localStorage 內容自動合併進今日出席名單

修法:
- match.ts 移除跨區平衡三輪邏輯,移植 buildSchedule/pickBestMatching 等演算法,
  新增 buildCourtGroups 輸出場地格式;RoundGroup 增加 label
- convertDbRecordToGroups 依 personnel 是否含 B 區自動分流 V1 / V2
- ScoreboardPage 的 getPresetTeamSelectionSlot / removePresetTeamFromDraft
  改為兩人同隊即成立、不看順序

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 12:19:53 +08:00
co-authored by Claude Fable 5
parent 6ae1eda7e2
commit 6f38c155f0
9 changed files with 546 additions and 163 deletions
+50
View File
@@ -12,6 +12,8 @@ const matchTableName = process.env.DB_TABLE ?? 'badminton'
const historyTableName = process.env.DB_HISTORY_TABLE ?? 'history'
// 出席統計的來源:TG 報名 bot 寫入的 attendance 表(每天每人一列)。
const attendanceTableName = process.env.DB_ATTENDANCE_TABLE ?? 'attendance'
// 成員實力表(match-hub 維護的 tg_members.skill)。
const membersTableName = process.env.DB_MEMBERS_TABLE ?? 'tg_members'
const appVersion = process.env.APP_VERSION ?? `${Date.now()}`
const appStartedAt = new Date().toISOString()
const LIVE_ROOM_STALE_MS = 30_000
@@ -380,6 +382,43 @@ app.get('/api/match-results/:time', async (request, response) => {
}
})
// 全體成員實力表(1~10,預設 1),給首頁手動排賽程時做實力配對用。
app.get('/api/skills', async (_request, response) => {
if (!pool) {
response.status(500).json({
ok: false,
message: `DB 尚未設定完成,缺少 ${missingEnv.join(', ')}`,
})
return
}
try {
const [rows] = await pool.execute(
`SELECT nickname, skill FROM \`${membersTableName}\` WHERE nickname IS NOT NULL`,
)
const skills = {}
rows.forEach((row) => {
const name = String(row.nickname ?? '').trim()
if (name) {
skills[name] = clampSkill(row.skill)
}
})
response.json({
ok: true,
data: { skills },
})
} catch (error) {
console.error('skills load error:', error)
response.status(500).json({
ok: false,
message: error instanceof Error ? error.message : '實力資料讀取失敗。',
})
}
})
app.get('/api/attendance', async (_request, response) => {
if (!pool) {
response.status(500).json({
@@ -895,6 +934,17 @@ function broadcastRoomList() {
})
}
// 實力限制在 1~10 的整數,缺值或壞值一律當 1。
function clampSkill(value) {
const skill = Number(value)
if (!Number.isFinite(skill)) {
return 1
}
return Math.min(10, Math.max(1, Math.round(skill)))
}
function getAttendanceName(rawName) {
const name = String(rawName ?? '').trim()
return ATTENDANCE_ALIASES[name] ?? name