import 'dotenv/config' import express from 'express' import mysql from 'mysql2/promise' import path from 'node:path' import { existsSync } from 'node:fs' import { fileURLToPath } from 'node:url' const app = express() const port = Number(process.env.PORT ?? process.env.SERVER_PORT ?? 8788) const matchTableName = process.env.DB_TABLE ?? 'badminton' const historyTableName = process.env.DB_HISTORY_TABLE ?? 'history' const currentFilePath = fileURLToPath(import.meta.url) const currentDir = path.dirname(currentFilePath) const projectRoot = path.resolve(currentDir, '..') const distDir = path.join(projectRoot, 'dist') const distReady = existsSync(path.join(distDir, 'index.html')) const requiredEnv = ['DB_HOST', 'DB_PORT', 'DB_USER', 'DB_PASSWORD', 'DB_DATABASE'] const missingEnv = requiredEnv.filter((key) => !process.env[key]) const pool = missingEnv.length === 0 ? mysql.createPool({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT), user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, charset: 'utf8mb4', waitForConnections: true, connectionLimit: 10, }) : null app.use(express.json()) app.get('/api/health', (_request, response) => { response.json({ ok: true, dbReady: Boolean(pool), distReady, historyTableName, matchTableName, missingEnv, }) }) app.get('/api/match-results/:time', async (request, response) => { if (!pool) { response.status(500).json({ ok: false, message: `DB 尚未設定完成,缺少 ${missingEnv.join(', ')}`, }) return } const time = String(request.params.time ?? '') if (!/^\d{8}$/.test(time)) { response.status(400).json({ ok: false, message: '日期格式必須是 YYYYMMDD。', }) return } try { await ensureMatchTable(pool, matchTableName) const [rows] = await pool.execute( `SELECT time, personnel, battlecombination FROM \`${matchTableName}\` WHERE time = ? LIMIT 1`, [Number(time)], ) const record = rows[0] if (!record) { response.status(404).json({ ok: false, message: '指定日期沒有資料。', }) return } response.json({ ok: true, data: record, }) } catch (error) { console.error('match-results load error:', error) response.status(500).json({ ok: false, message: error instanceof Error ? error.message : '讀取對戰資料失敗。', }) } }) app.post('/api/history', async (request, response) => { if (!pool) { response.status(500).json({ ok: false, message: `DB 尚未設定完成,缺少 ${missingEnv.join(', ')}`, }) return } const { dayOfWeek, players, score, scoreList, team, time, type, winScore, } = request.body ?? {} if ( typeof time !== 'number' || typeof dayOfWeek !== 'number' || typeof winScore !== 'number' || typeof type !== 'number' || !Array.isArray(score) || score.length !== 2 || !Array.isArray(players) || !Array.isArray(team) || !Array.isArray(scoreList) ) { response.status(400).json({ ok: false, message: '戰績資料格式不正確。', }) return } try { await ensureHistoryTable(pool, historyTableName) const [result] = await pool.execute( ` INSERT INTO \`${historyTableName}\` (time, dayOfWeek, score, winScore, type, players, team, scoreList) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, [ time, dayOfWeek, JSON.stringify(score), winScore, type, JSON.stringify(players), JSON.stringify(team), JSON.stringify(scoreList), ], ) response.json({ ok: true, data: { id: result.insertId, }, message: '戰績已寫入 DB。', }) } catch (error) { console.error('history save error:', error) response.status(500).json({ ok: false, message: error instanceof Error ? error.message : '寫入戰績失敗。', }) } }) app.get('/api/history', async (_request, response) => { if (!pool) { response.status(500).json({ ok: false, message: `DB 尚未設定完成,缺少 ${missingEnv.join(', ')}`, }) return } try { await ensureHistoryTable(pool, historyTableName) const [rows] = await pool.execute( ` SELECT id, time, dayOfWeek, score, winScore, type, players, team, scoreList FROM \`${historyTableName}\` ORDER BY id DESC `, ) response.json({ ok: true, data: rows, }) } catch (error) { console.error('history load error:', error) response.status(500).json({ ok: false, message: error instanceof Error ? error.message : '讀取歷史戰績失敗。', }) } }) app.delete('/api/history/:id', async (request, response) => { if (!pool) { response.status(500).json({ ok: false, message: `DB 設定不完整,缺少:${missingEnv.join(', ')}`, }) return } const id = Number(request.params.id) if (!Number.isInteger(id) || id <= 0) { response.status(400).json({ ok: false, message: '戰績編號格式不正確。', }) return } try { await ensureHistoryTable(pool, historyTableName) const [result] = await pool.execute( `DELETE FROM \`${historyTableName}\` WHERE id = ? LIMIT 1`, [id], ) if (result.affectedRows === 0) { response.status(404).json({ ok: false, message: '找不到要刪除的戰績。', }) return } response.json({ ok: true, message: '戰績已刪除。', }) } catch (error) { console.error('history delete error:', error) response.status(500).json({ ok: false, message: error instanceof Error ? error.message : '刪除戰績失敗。', }) } }) if (distReady) { app.use(express.static(distDir)) app.get(/^(?!\/api).*/, (_request, response) => { response.sendFile(path.join(distDir, 'index.html')) }) } app.listen(port, () => { console.log(`Server ready on http://localhost:${port}`) if (missingEnv.length > 0) { console.log(`Missing env: ${missingEnv.join(', ')}`) } }) async function ensureMatchTable(poolInstance, currentTableName) { await poolInstance.execute(` CREATE TABLE IF NOT EXISTS \`${currentTableName}\` ( time INT(11) NOT NULL, personnel TEXT NOT NULL, battlecombination TEXT DEFAULT NULL, PRIMARY KEY (time) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci `) } async function ensureHistoryTable(poolInstance, currentTableName) { await poolInstance.execute(` CREATE TABLE IF NOT EXISTS \`${currentTableName}\` ( id INT(11) NOT NULL AUTO_INCREMENT, time INT(11) NOT NULL COMMENT '記錄時間', dayOfWeek INT(1) NOT NULL COMMENT '星期', score VARCHAR(255) NOT NULL COMMENT '隊伍分數 [ [隊伍1分數], [隊伍2分數] ]', winScore INT(2) NOT NULL COMMENT '幾分獲勝', type INT(1) NOT NULL COMMENT '遊戲類型(0:雙打,1:單打)', players VARCHAR(255) NOT NULL COMMENT '玩家', team VARCHAR(255) NOT NULL COMMENT '玩家隊伍 [ [隊伍1成員], [隊伍2成員] ]', scoreList TEXT DEFAULT NULL COMMENT '得分過程[round, starter, winCount, winner]', PRIMARY KEY (id) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci `) }