141 lines
3.7 KiB
JavaScript
141 lines
3.7 KiB
JavaScript
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 ?? 8787)
|
|
const tableName = process.env.DB_TABLE ?? 'badminton'
|
|
|
|
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,
|
|
missingEnv,
|
|
})
|
|
})
|
|
|
|
app.post('/api/match-results', async (request, response) => {
|
|
if (!pool) {
|
|
response.status(500).json({
|
|
ok: false,
|
|
message: `資料庫環境變數缺少:${missingEnv.join(', ')}`,
|
|
})
|
|
return
|
|
}
|
|
|
|
const { time, areaA, areaB, teams } = request.body ?? {}
|
|
|
|
if (
|
|
typeof time !== 'string' ||
|
|
!Array.isArray(areaA) ||
|
|
!Array.isArray(areaB) ||
|
|
!Array.isArray(teams)
|
|
) {
|
|
response.status(400).json({
|
|
ok: false,
|
|
message: '送出的資料格式不正確。',
|
|
})
|
|
return
|
|
}
|
|
|
|
try {
|
|
await ensureTable(pool, tableName)
|
|
const personnel = JSON.stringify([
|
|
...areaA.map((name) => [1, name]),
|
|
...areaB.map((name) => [0, name]),
|
|
])
|
|
const battlecombination = JSON.stringify(
|
|
Object.fromEntries(
|
|
teams.map((round, index) => [
|
|
String(index),
|
|
round.teams.map((team) => [team.a, team.b]),
|
|
]),
|
|
),
|
|
)
|
|
|
|
await pool.execute(
|
|
`
|
|
INSERT INTO \`${tableName}\` (time, personnel, battlecombination)
|
|
VALUES (?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
personnel = VALUES(personnel),
|
|
battlecombination = VALUES(battlecombination)
|
|
`,
|
|
[Number(time), personnel, battlecombination],
|
|
)
|
|
|
|
response.json({
|
|
ok: true,
|
|
message: '已寫入資料庫。',
|
|
})
|
|
} catch (error) {
|
|
console.error('match-results 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'))
|
|
})
|
|
} else {
|
|
app.get('/', (_request, response) => {
|
|
response
|
|
.status(503)
|
|
.send('前端尚未建置,請先執行 npm run build 或使用 Docker 映像部署。')
|
|
})
|
|
}
|
|
|
|
app.listen(port, () => {
|
|
console.log(`Server ready on http://localhost:${port}`)
|
|
console.log(`Static files: ${distReady ? 'loaded' : 'missing'}`)
|
|
if (missingEnv.length > 0) {
|
|
console.log(`Missing env: ${missingEnv.join(', ')}`)
|
|
}
|
|
})
|
|
|
|
async function ensureTable(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
|
|
`)
|
|
}
|