| import type { ClientToServerEvents, ServerToClientEvents } from '@music-together/shared' |
| import cors from 'cors' |
| import express from 'express' |
| import fs from 'node:fs' |
| import { createServer } from 'node:http' |
| import path from 'node:path' |
| import { fileURLToPath } from 'node:url' |
| import { TypedServer } from './wss.js' |
| import { config } from './config.js' |
| import { initializeSocket } from './controllers/index.js' |
| import { identityHttpMiddleware } from './middleware/identityHttp.js' |
| import { attachSocketIdentity } from './middleware/socketIdentity.js' |
| import type { SocketData } from './middleware/types.js' |
| import authRoutes from './routes/auth.js' |
| import { createAdminRoutes } from './routes/admin.js' |
| import { createAccountRoutes } from './routes/account.js' |
| import musicRoutes from './routes/music.js' |
| import roomRoutes from './routes/rooms.js' |
| import settingsRoutes from './routes/settings.js' |
| import { clearAllTimers } from './services/roomLifecycleService.js' |
| import * as playerService from './services/playerService.js' |
| import { |
| startTencentCredentialRefreshScheduler, |
| stopTencentCredentialRefreshScheduler, |
| } from './services/tencentCredentialRefreshService.js' |
| import { logger } from './utils/logger.js' |
| import { databasePath } from './repositories/database.js' |
|
|
| const app = express() |
| const httpServer = createServer(app) |
| const io = new TypedServer<ClientToServerEvents, ServerToClientEvents, SocketData>(httpServer) |
|
|
| |
| |
| |
| app.use( |
| cors({ |
| origin: config.explicitOrigins.length > 0 ? config.explicitOrigins : (true as const), |
| credentials: true, |
| }), |
| ) |
| |
| |
| app.use(express.json({ limit: '9mb' })) |
| app.use('/api', identityHttpMiddleware) |
| app.use('/uploads/avatars', express.static(path.join(path.dirname(databasePath), 'avatars'), { maxAge: '1h' })) |
| app.use('/uploads/backgrounds', express.static(path.join(path.dirname(databasePath), 'backgrounds'), { maxAge: '1h' })) |
|
|
| |
| app.use('/api/auth', authRoutes) |
| app.use('/api/auth', createAccountRoutes(io)) |
| app.use('/api/admin', createAdminRoutes(io)) |
| app.use('/api/settings', settingsRoutes) |
| app.use('/api/music', musicRoutes) |
| app.use('/api/rooms', roomRoutes) |
|
|
| |
| app.get('/api/health', (_req, res) => { |
| res.json({ status: 'ok', timestamp: Date.now() }) |
| }) |
|
|
| |
| app.get('/api/version', (_req, res) => { |
| res.json({ version: config.version }) |
| }) |
|
|
| |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)) |
| const clientDist = path.resolve(__dirname, '../../client/dist') |
| const indexHtml = path.join(clientDist, 'index.html') |
|
|
| if (fs.existsSync(indexHtml)) { |
| |
| app.use( |
| '/assets', |
| express.static(path.join(clientDist, 'assets'), { |
| maxAge: '1y', |
| immutable: true, |
| }), |
| ) |
| |
| app.use( |
| express.static(clientDist, { |
| maxAge: '1h', |
| setHeaders: (res, filePath) => { |
| if (filePath.endsWith('index.html')) { |
| res.setHeader('Cache-Control', 'no-cache, must-revalidate') |
| } |
| }, |
| }), |
| ) |
| |
| app.get('*', (_req, res) => { |
| res.setHeader('Cache-Control', 'no-cache, must-revalidate') |
| res.sendFile(indexHtml) |
| }) |
| logger.info('客户端静态页面已加载', { clientDist }) |
| } else { |
| logger.info('未发现客户端构建产物,已跳过静态页面托管(开发模式)') |
| } |
|
|
| attachSocketIdentity(io) |
| initializeSocket(io) |
|
|
| |
| const playbackPersistenceTimer = setInterval(() => playerService.persistPlaybackSnapshots(), 5_000) |
| playbackPersistenceTimer.unref() |
| const tencentCredentialRefreshTimer = startTencentCredentialRefreshScheduler() |
|
|
| httpServer.on('error', (err: NodeJS.ErrnoException) => { |
| if (err.code === 'EADDRINUSE') { |
| logger.error(`Port ${config.port} already in use`) |
| process.exit(1) |
| } |
| throw err |
| }) |
|
|
| httpServer.listen(config.port, () => { |
| logger.info(`服务器已启动,监听端口 ${config.port}`, { |
| event: 'server.started', |
| port: config.port, |
| environment: config.isProd ? 'production' : 'development', |
| version: config.version, |
| }) |
| logger.info( |
| config.explicitOrigins.length > 0 |
| ? `仅允许以下来源连接:${config.explicitOrigins.join(', ')}` |
| : '当前允许所有来源连接(自动模式)', |
| ) |
| }) |
|
|
| |
| function shutdown(signal: string) { |
| logger.info(`收到 ${signal} 信号,正在安全关闭服务器……`) |
| clearInterval(playbackPersistenceTimer) |
| stopTencentCredentialRefreshScheduler(tencentCredentialRefreshTimer) |
| playerService.persistPlaybackSnapshots() |
| clearAllTimers() |
| io.close(() => { |
| httpServer.close(() => { |
| logger.info('服务器已关闭') |
| process.exit(0) |
| }) |
| }) |
| |
| setTimeout(() => process.exit(1), 10_000).unref() |
| } |
|
|
| process.on('SIGTERM', () => shutdown('SIGTERM')) |
| process.on('SIGINT', () => shutdown('SIGINT')) |
|
|