arcteryx-game/server.ts
2025-08-04 11:34:29 +08:00

177 lines
4.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import express from 'express';
import { promises as fs } from 'fs';
import path from 'path';
const app = express();
const PORT = process.env.PORT || 3001;
const SCORES_FILE = path.join(__dirname, 'scores.json');
// 分数数据类型
interface ScoreEntry {
name: string;
time: number;
region: string;
store: string;
timestamp: number;
}
// 中间件
app.use(express.json());
app.use(express.static('dist')); // 服务静态文件
// 初始化分数文件
async function initScoresFile() {
try {
await fs.access(SCORES_FILE);
} catch {
// 文件不存在,创建空数组
await fs.writeFile(SCORES_FILE, JSON.stringify([]));
}
}
// 读取分数数据
async function readScores(): Promise<ScoreEntry[]> {
try {
const data = await fs.readFile(SCORES_FILE, 'utf-8');
return JSON.parse(data);
} catch {
return [];
}
}
// 写入分数数据
async function writeScores(scores: ScoreEntry[]): Promise<void> {
await fs.writeFile(SCORES_FILE, JSON.stringify(scores, null, 2));
}
// POST /api/score - 提交分数
app.post('/api/score', async (req, res) => {
try {
const { region, store, name, time } = req.body;
// 验证必要字段
if (!region || !store || !name || typeof time !== 'number') {
return res.status(400).json({
error: '缺少必要字段: region, store, name, time'
});
}
// 验证时间值
if (time <= 0) {
return res.status(400).json({
error: '时间必须大于0'
});
}
const scores = await readScores();
// 创建新的分数记录
const newScore: ScoreEntry = {
name: String(name).trim(),
time: Number(time),
region: String(region).trim(),
store: String(store).trim(),
timestamp: Date.now()
};
// 检查是否已有相同用户的记录
const existingIndex = scores.findIndex(
score => score.name === newScore.name &&
score.region === newScore.region &&
score.store === newScore.store
);
if (existingIndex !== -1) {
// 如果新时间更好(更短),则更新记录
if (newScore.time < scores[existingIndex].time) {
scores[existingIndex] = newScore;
}
} else {
// 添加新记录
scores.push(newScore);
}
await writeScores(scores);
res.json({
success: true,
message: '分数提交成功',
score: newScore
});
} catch (error) {
console.error('提交分数时出错:', error);
res.status(500).json({
error: '服务器内部错误'
});
}
});
// GET /api/score - 获取排行榜前8名
app.get('/api/score', async (req, res) => {
try {
const scores = await readScores();
// 按时间排序时间短的在前并取前8名
const leaderBoard = scores
.sort((a, b) => a.time - b.time)
.slice(0, 8)
.map(score => ({
name: score.name,
time: score.time,
region: score.region,
store: score.store
}));
res.json(leaderBoard);
} catch (error) {
console.error('获取排行榜时出错:', error);
res.status(500).json({
error: '服务器内部错误'
});
}
});
// GET /api/score/all - 获取所有分数(可选,用于管理)
app.get('/api/score/all', async (req, res) => {
try {
const scores = await readScores();
res.json(scores.sort((a, b) => a.time - b.time));
} catch (error) {
console.error('获取所有分数时出错:', error);
res.status(500).json({
error: '服务器内部错误'
});
}
});
// 健康检查端点
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString()
});
});
// 处理 SPA 路由 - 将所有未匹配的路由返回 index.html
app.get('/*', (req, res) => {
try {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
} catch (error) {
res.status(404).send('Page not found');
}
});
// 启动服务器
async function startServer() {
await initScoresFile();
app.listen(PORT, () => {
console.log(`🚀 服务器运行在 http://localhost:${PORT}`);
console.log(`📊 API 端点:`);
console.log(` POST /api/score - 提交分数`);
console.log(` GET /api/score - 获取排行榜`);
console.log(` GET /api/health - 健康检查`);
});
}
startServer().catch(console.error);