gwy-exam/server/src/server.ts

78 lines
3.2 KiB
TypeScript
Raw 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.

// 最先加载 server/.envdotenv确保 DATA_DIR / PORT / AI_* 在模块读取前就位
import './env.js'
import { pathToFileURL } from 'node:url'
import Fastify from 'fastify'
import cors from '@fastify/cors'
import swagger from '@fastify/swagger'
import swaggerUi from '@fastify/swagger-ui'
import { initializeData } from './data/seed.js'
import { registerRoutes, routeCount } from './routes.js'
import { errorHandler, notFoundHandler } from './errors.js'
/** 构建 Fastify 实例(不监听端口,供运行与 OpenAPI 导出共用) */
export async function buildServer() {
const app = Fastify({ logger: true, exposeHeadRoutes: false })
// 允许「Content-Type: application/json 但无 body」的请求如 DELETE 常带该头但无体)。
// 默认 JSON 解析器遇到空 body 会抛 400「请求参数不合法」这里把空 body 视为 undefined避免误伤。
app.addContentTypeParser('application/json', { parseAs: 'string' }, (_req, body, done) => {
if (body === undefined || body === null || body === '') return done(null, undefined)
try {
done(null, JSON.parse(body as string))
} catch (err) {
done(err as Error, undefined)
}
})
// 允许浏览器跨域调用本地后端GET/POST/PATCH/DELETE 均为现有接口用到的语义化方法)
await app.register(cors, {
origin: true,
methods: ['GET', 'HEAD', 'POST', 'PATCH', 'DELETE']
})
await app.register(swagger, {
openapi: {
info: {
title: '备考通 API',
description: '个人本地部署的公务员考试备考工具接口(语义化路径,统一错误格式)',
version: '1.0.0'
},
servers: [{ url: `http://localhost:${process.env.PORT ?? 3000}`, description: '本地服务' }],
tags: [
{ name: '数据中枢', description: '备考数据概览与学习趋势' },
{ name: '刷题', description: '模块组卷、作答与结果' },
{ name: '错题复习', description: '错题沉淀与间隔复习' },
{ name: '备考计划', description: '今日与本周任务' },
{ name: '模考分析', description: '行测模考成绩与分析' },
{ name: '要闻', description: '时政要闻列表与导入' },
{ name: '题库管理', description: '题目导入导出与删除' },
{ name: 'AI 讲解', description: 'AI 题目讲解Token 仅在后端配置)' },
{ name: '个人档案', description: '学习档案' },
{ name: '设置', description: '偏好设置' }
]
}
})
await app.register(swaggerUi, { routePrefix: '/api/docs' })
app.setErrorHandler(errorHandler)
app.setNotFoundHandler(notFoundHandler)
// OpenAPI 文档端点(需求文档 2.2 约定的访问地址)
app.get('/api/openapi.json', async () => app.swagger())
app.get('/health', async () => ({ status: 'ok', service: 'gwy-server', routes: routeCount }))
await app.register(registerRoutes)
return app
}
const invokedDirectly =
process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href
if (invokedDirectly) {
const app = await buildServer()
await initializeData()
await app.listen({ port: Number(process.env.PORT ?? 3000), host: process.env.HOST ?? '0.0.0.0' })
}