Refine scoreboard flow and update ports
This commit is contained in:
213
server/server.mjs
Normal file
213
server/server.mjs
Normal file
@@ -0,0 +1,213 @@
|
||||
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 : '寫入戰績失敗。',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
`)
|
||||
}
|
||||
Reference in New Issue
Block a user