import express from 'express' import process from 'node:process' import { runtimeConfig } from './config/runtime.js' import { runDatabaseMigrations } from './db/migrate.js' import adminRouter from './routes/admin.js' import claimsRouter from './routes/claims.js' import webhooksRouter from './routes/webhooks.js' import { ensureAdminUsersBootstrapped } from './services/admin/admin-auth-service.js' import { repairAgisoOrderData } from './services/order/order-repair-service.js' import { closeLocalOcrWorker, warmupLocalOcrWorker } from './services/session/ocr.js' import { closeAllTencentBrowserSessions, warmupTencentBrowser } from './services/session/session.js' import { logError, logInfo, logWarn } from './utils/logger.js' import tencentRouter from './routes/tencent.js' const app = express() const port = Number(runtimeConfig.server.port || 3000) runDatabaseMigrations() await ensureAdminUsersBootstrapped() await repairHistoricalOrders() app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*') res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization') res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS') if (req.method === 'OPTIONS') { res.sendStatus(204) return } next() }) app.use(express.json({ limit: '2mb' })) app.use(express.urlencoded({ extended: true })) app.get('/health', (_req, res) => { res.json({ code: 0, msg: 'ok', time: Math.floor(Date.now() / 1000), }) }) app.use('/api/v1/tencent', tencentRouter) app.use('/api/v1/webhooks', webhooksRouter) app.use('/api/v1/claim', claimsRouter) app.use('/api/v1/admin', adminRouter) const server = app.listen(port, () => { logInfo('[startup]', `order-site-backend listening on http://127.0.0.1:${port}`) void bootstrapBrowser() void bootstrapOcr() }) async function bootstrapBrowser() { try { const result = await warmupTencentBrowser() if (!result.warmed) { logInfo('[startup]', 'browser prewarm skipped') return } logInfo('[startup]', 'browser prewarm ready') } catch (error) { if (isMissingPlaywrightBrowserError(error)) { const installCommand = resolveBrowserInstallCommand() logWarn('[startup]', `browser prewarm skipped: ${formatErrorMessage(error)}`) logWarn('[startup]', `请先安装浏览器依赖: ${installCommand}`) if (String(runtimeConfig.browser.chromePath || '').trim()) { logWarn('[startup]', '当前设置了 CHROME_PATH,也请确认该路径指向的浏览器可执行文件真实存在') } return } logError('[startup]', 'browser prewarm failed', error) } } async function bootstrapOcr() { try { await warmupLocalOcrWorker() logInfo('[startup]', 'local OCR ready') } catch (error) { logWarn('[startup]', `local OCR skipped: ${formatStartupError(error)}`) } } async function repairHistoricalOrders() { try { await repairAgisoOrderData() } catch (error) { logWarn('[startup]', `历史 Agiso 咸鱼订单修复跳过: ${formatStartupError(error)}`) } } function formatStartupError(error) { if (error instanceof Error && error.message) { return error.message.split('\n')[0].trim() } return String(error || '未知错误').trim() } function isMissingPlaywrightBrowserError(error) { const message = formatErrorMessage(error).toLowerCase() return ( message.includes("executable doesn't exist") || message.includes('please run the following command to download new browsers') || message.includes('browserType.launch'.toLowerCase()) && message.includes('please run') ) } function formatErrorMessage(error) { if (error instanceof Error && error.message) { return error.message.split('\n')[0].trim() } return String(error || '未知错误').trim() } function resolveBrowserInstallCommand() { if (process.platform === 'linux') { return 'npm run browser:install:linux' } return 'npm run browser:install' } let shutdownStarted = false async function shutdown(signal) { if (shutdownStarted) { return } shutdownStarted = true logInfo('[shutdown]', `received ${signal}, closing browser sessions and HTTP server`) try { await closeAllTencentBrowserSessions({ markClosed: true }) } catch (error) { logError('[shutdown]', 'failed to close browser sessions', error) } try { await closeLocalOcrWorker() } catch (error) { logError('[shutdown]', 'failed to close local OCR worker', error) } if (!server.listening) { return } await new Promise((resolve) => { server.close((error) => { if (error) { logError('[shutdown]', 'failed to close HTTP server', error) process.exitCode = 1 } resolve() }) }) } process.on('SIGINT', () => { void shutdown('SIGINT') }) process.on('SIGTERM', () => { void shutdown('SIGTERM') })