import 'dotenv/config' import path from 'node:path' import fs from 'node:fs' import express from 'express' import cors from 'cors' import otaRouter from './routes/ota.js' import appRouter from './routes/app.js' import { initDb } from './db.js' // 默认 3002: 与 iboard (3001) 同机部署时不冲突 const PORT = Number(process.env.PORT) || 3002 const DIST_DIR = path.join(process.cwd(), 'dist') const HAS_DIST = fs.existsSync(DIST_DIR) const app = express() app.use(cors()) app.use(express.json()) // 健康检查 (容器活性探针统一用 /healthz, 保留 /api/health 兼容旧引用) app.get('/healthz', (_req, res) => res.json({ ok: true, ts: Date.now() })) app.get('/api/health', (_req, res) => res.json({ ok: true, ts: Date.now() })) // 业务接口 (客户端数据读写, 登录鉴权) app.use('/api', appRouter) // OTA 接口公开 (检查/下载更新不需要登录), 必须挂载在最后以防 /api 路由冲突 app.use('/api', otaRouter) // 静态资源 (Vite build 产物) if (HAS_DIST) { app.use(express.static(DIST_DIR, { index: false, maxAge: '1h', etag: true })) // SPA fallback: 非 /api/* 的请求一律返回 index.html, 由前端路由处理 // 必须 no-cache: 否则浏览器/代理缓存旧 index.html, 引用的仍是旧哈希资源, // 与部署的 dist 产生"版本不一致"(web 用户刷新也拿不到新页面) app.get(/^\/(?!api\/).*/, (_req, res, next) => { const indexHtml = path.join(DIST_DIR, 'index.html') if (!fs.existsSync(indexHtml)) return next() res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate') res.sendFile(indexHtml) }) } // 统一错误处理 app.use((err, _req, res, _next) => { console.error('[api error]', err) const code = err && err.status ? Number(err.status) : 500 if (code >= 500) console.error(err.stack || err) res.status(code).json({ error: (err && err.message) || '服务器内部错误' }) }) initDb() .then(() => { app.listen(PORT, () => { console.log(`[wardrobe-api] listening on http://127.0.0.1:${PORT}`) console.log(`[wardrobe-api] static dist: ${HAS_DIST ? DIST_DIR : '(not built, API only)'}`) }) }) .catch((e) => { console.error('[db] failed to init DuckDB:', e) process.exit(1) })