gwy-exam/server/src/routes.ts
2026-08-31 16:48:49 +08:00

274 lines
10 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.

import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
import { z } from 'zod'
import { ErrorResponseSchema } from './schemas/api.js'
import { MockExamSchema, NewsItemSchema, StudyPlanSchema } from './schemas/entities.js'
import * as schemas from './schemas/api.js'
import * as dashboard from './handlers/dashboard.js'
import * as practice from './handlers/practice.js'
import * as review from './handlers/review.js'
import * as plans from './handlers/plans.js'
import * as mock from './handlers/mock.js'
import * as news from './handlers/news.js'
import * as questions from './handlers/questions.js'
import * as ai from './handlers/ai.js'
import * as profile from './handlers/profile.js'
type RouteDef = {
method: 'GET' | 'POST' | 'PATCH' | 'DELETE'
url: string
summary: string
tags: string[]
params?: z.ZodType
query?: z.ZodType
body?: z.ZodType
response: Record<string, z.ZodType>
handler: (request: FastifyRequest, reply: FastifyReply) => unknown
}
/**
* 全部业务路由的唯一登记表。
* - 请求/响应 Schema 以 Zod 定义(单一来源),注册时转换为 JSON Schema
* 请求侧用 io=input 交给 Ajv 校验,响应侧用 io=output 交给序列化与 OpenAPI 文档。
* - 每个接口自动附带 400 / 404 统一错误响应 Schema保证“没有无 Schema 的接口”。
*/
const routes: RouteDef[] = [
// 数据中枢
{
method: 'GET', url: '/api/dashboard/overview', summary: '备考概览', tags: ['数据中枢'],
response: { 200: schemas.DashboardOverviewResponseSchema },
handler: dashboard.overview
},
{
method: 'GET', url: '/api/dashboard/trends', summary: '学习趋势', tags: ['数据中枢'],
query: schemas.DashboardTrendsQuerySchema,
response: { 200: schemas.DashboardTrendsResponseSchema },
handler: dashboard.trends
},
// 刷题
{
method: 'GET', url: '/api/practice/modules', summary: '行测模块与题量', tags: ['刷题'],
response: { 200: schemas.PracticeModulesResponseSchema },
handler: practice.modules
},
{
method: 'POST', url: '/api/practice/start', summary: '开始刷题(模块组卷)', tags: ['刷题'],
body: schemas.PracticeStartBodySchema,
response: { 200: schemas.PracticeSessionViewSchema },
handler: practice.start
},
{
method: 'GET', url: '/api/practice/:sessionId/question', summary: '获取当前题目', tags: ['刷题'],
params: schemas.SessionIdParamsSchema,
response: { 200: schemas.PracticeQuestionResponseSchema },
handler: practice.currentQuestion
},
{
method: 'POST', url: '/api/practice/:sessionId/answer', summary: '提交单题作答', tags: ['刷题'],
params: schemas.SessionIdParamsSchema,
body: schemas.PracticeAnswerBodySchema,
response: { 200: schemas.PracticeAnswerResponseSchema },
handler: practice.answer
},
{
method: 'POST', url: '/api/practice/:sessionId/finish', summary: '结束会话并返回结果', tags: ['刷题'],
params: schemas.SessionIdParamsSchema,
response: { 200: schemas.PracticeFinishResponseSchema },
handler: practice.finish
},
// 错题复习
{
method: 'GET', url: '/api/review/list', summary: '错题列表', tags: ['错题复习'],
query: schemas.ReviewListQuerySchema,
response: { 200: schemas.ReviewListResponseSchema },
handler: review.list
},
{
method: 'POST', url: '/api/review/:id/submit', summary: '提交复习作答', tags: ['错题复习'],
params: schemas.WrongQuestionIdParamsSchema,
body: schemas.ReviewSubmitBodySchema,
response: { 200: schemas.ReviewSubmitResponseSchema },
handler: review.submit
},
{
method: 'POST', url: '/api/review/:id/mark-mastered', summary: '标记已掌握', tags: ['错题复习'],
params: schemas.WrongQuestionIdParamsSchema,
response: { 200: schemas.ReviewMarkResponseSchema },
handler: review.markMastered
},
// 备考计划
{
method: 'GET', url: '/api/plans/today', summary: '今日任务', tags: ['备考计划'],
query: schemas.PlansTodayQuerySchema,
response: { 200: schemas.PlansTodayResponseSchema },
handler: plans.today
},
{
method: 'GET', url: '/api/plans/week', summary: '本周任务', tags: ['备考计划'],
query: schemas.PlansWeekQuerySchema,
response: { 200: schemas.PlansWeekResponseSchema },
handler: plans.week
},
{
method: 'POST', url: '/api/plans/tasks', summary: '创建计划任务', tags: ['备考计划'],
body: schemas.PlanCreateBodySchema,
response: { 200: StudyPlanSchema },
handler: plans.createTask
},
{
method: 'PATCH', url: '/api/plans/tasks/:id/toggle', summary: '切换任务完成状态', tags: ['备考计划'],
params: schemas.PlanTaskIdParamsSchema,
response: { 200: schemas.PlanToggleResponseSchema },
handler: plans.toggleTask
},
// 模考分析
{
method: 'GET', url: '/api/mock-exams/analysis', summary: '模考成绩分析', tags: ['模考分析'],
response: { 200: schemas.MockExamAnalysisResponseSchema },
handler: mock.analysis
},
{
method: 'POST', url: '/api/mock-exams/record', summary: '录入模考成绩', tags: ['模考分析'],
body: schemas.MockExamRecordBodySchema,
response: { 200: MockExamSchema },
handler: mock.record
},
// 要闻
{
method: 'GET', url: '/api/news/list', summary: '要闻列表', tags: ['要闻'],
query: schemas.NewsListQuerySchema,
response: { 200: schemas.NewsListResponseSchema },
handler: news.list
},
{
method: 'GET', url: '/api/news/:id', summary: '要闻详情', tags: ['要闻'],
params: schemas.NewsIdParamsSchema,
response: { 200: NewsItemSchema },
handler: news.detail
},
{
method: 'POST', url: '/api/news/import/json', summary: 'JSON 导入要闻', tags: ['要闻'],
body: schemas.NewsImportJsonBodySchema,
response: { 200: schemas.ImportResultSchema },
handler: news.importJson
},
{
method: 'POST', url: '/api/news/import/rss', summary: 'RSS 导入入口', tags: ['要闻'],
body: schemas.NewsImportRssBodySchema,
response: { 200: schemas.ImportResultSchema },
handler: news.importRss
},
{
method: 'POST', url: '/api/news/import/api', summary: 'API 导入入口', tags: ['要闻'],
body: schemas.NewsImportApiBodySchema,
response: { 200: schemas.ImportResultSchema },
handler: news.importApi
},
{
method: 'POST', url: '/api/news/import/url', summary: 'URL 导入入口', tags: ['要闻'],
body: schemas.NewsImportUrlBodySchema,
response: { 200: schemas.ImportResultSchema },
handler: news.importUrl
},
// 题库管理
{
method: 'GET', url: '/api/questions/list', summary: '题目列表', tags: ['题库管理'],
query: schemas.QuestionsListQuerySchema,
response: { 200: schemas.QuestionsListResponseSchema },
handler: questions.list
},
{
method: 'POST', url: '/api/questions/import', summary: '题目 JSON 导入', tags: ['题库管理'],
body: schemas.QuestionsImportBodySchema,
response: { 200: schemas.ImportResultSchema },
handler: questions.importQuestions
},
{
method: 'GET', url: '/api/questions/export', summary: '导出当前题库', tags: ['题库管理'],
response: { 200: schemas.QuestionsExportResponseSchema },
handler: questions.exportQuestions
},
{
method: 'DELETE', url: '/api/questions/:id', summary: '删除题目', tags: ['题库管理'],
params: schemas.QuestionIdParamsSchema,
response: { 200: schemas.QuestionsDeleteResponseSchema },
handler: questions.remove
},
// AI 讲解
{
method: 'POST', url: '/api/ai/explain-question', summary: 'AI 题目讲解', tags: ['AI 讲解'],
body: schemas.AiExplainBodySchema,
response: { 200: schemas.AiExplainResponseSchema },
handler: ai.explainQuestion
},
// 个人档案 / 设置
{
method: 'GET', url: '/api/profile', summary: '个人档案', tags: ['个人档案'],
response: { 200: schemas.ProfileResponseSchema },
handler: profile.getProfile
},
{
method: 'PATCH', url: '/api/profile', summary: '修改个人档案', tags: ['个人档案'],
body: schemas.ProfilePatchBodySchema,
response: { 200: schemas.ProfileResponseSchema },
handler: profile.patchProfile
},
{
method: 'GET', url: '/api/settings', summary: '获取设置', tags: ['设置'],
response: { 200: schemas.SettingsResponseSchema },
handler: profile.getSettings
},
{
method: 'PATCH', url: '/api/settings', summary: '修改设置', tags: ['设置'],
body: schemas.SettingsPatchBodySchema,
response: { 200: schemas.SettingsResponseSchema },
handler: profile.patchSettings
}
]
const toJsonSchema = (schema: z.ZodType | undefined, io: 'input' | 'output') => {
if (!schema) return undefined
return z.toJSONSchema(schema, { io, target: 'draft-7', unrepresentable: 'any' })
}
const errorJsonSchema = z.toJSONSchema(ErrorResponseSchema, { io: 'output', target: 'draft-7', unrepresentable: 'any' })
export async function registerRoutes(app: FastifyInstance) {
for (const route of routes) {
app.route({
method: route.method,
url: route.url,
schema: {
summary: route.summary,
tags: route.tags,
// 仅在确有 schema 时声明 params/querystring若声明为空对象@fastify/swagger
// 会把 `{ type: 'object' }` 的 "type" 关键字误当作参数名,给每个路由生成虚假参数。
...(route.params ? { params: toJsonSchema(route.params, 'input') } : {}),
...(route.query ? { querystring: toJsonSchema(route.query, 'input') } : {}),
...(route.method === 'GET' || route.method === 'DELETE' || !route.body
? {}
: { body: toJsonSchema(route.body, 'input') }),
response: Object.fromEntries(
Object.entries({
200: toJsonSchema(route.response[200], 'output'),
400: route.body || route.query || route.params ? errorJsonSchema : undefined,
404: errorJsonSchema,
500: errorJsonSchema
}).filter(([, value]) => value !== undefined)
)
},
handler: route.handler
})
}
}
/** 已登记接口数量(供导出脚本与检查使用) */
export const routeCount = routes.length