From b637ea9ef00aaca7e253b4ed672145cba01ec3c7 Mon Sep 17 00:00:00 2001 From: liyy <18435186204@163.com> Date: Fri, 28 Aug 2026 11:57:21 +0800 Subject: [PATCH] feat: add json data store and demo data --- docs/checks/task-02.md | 21 ++++++++++++++++ server/src/data/files.ts | 17 +++++++++++++ server/src/data/seed.ts | 50 +++++++++++++++++++++++++++++++++++++++ server/src/data/store.ts | 34 ++++++++++++++++++++++++++ server/src/server.ts | 2 ++ server/src/types/index.ts | 21 ++++++++++++++++ 6 files changed, 145 insertions(+) create mode 100644 docs/checks/task-02.md create mode 100644 server/src/data/files.ts create mode 100644 server/src/data/seed.ts create mode 100644 server/src/data/store.ts create mode 100644 server/src/types/index.ts diff --git a/docs/checks/task-02.md b/docs/checks/task-02.md new file mode 100644 index 0000000..07988ca --- /dev/null +++ b/docs/checks/task-02.md @@ -0,0 +1,21 @@ +# 任务 02 检查记录:JSON Store 与演示数据 + +日期:2026-08-28 + +## 命令检查 + +- `pnpm typecheck`:通过(server、client)。 +- `pnpm build`:通过(server、client)。 +- 后端启动后自动创建 `server/data/` 下 9 个 JSON 文件:通过。 +- 演示题库读取:通过,内置行测题目 3 道。 +- 演示要闻读取:通过,内置要闻 1 条。 +- 宿主机 IP `172.16.34.66:3000/health`:通过。 +- 停止服务后重新启动,数据文件仍可被读取:通过(文件由首次启动创建,未被覆盖)。 + +## 浏览器检查 + +任务 02 只涉及后端数据层,没有新增页面;已尝试回归检查现有 localhost 页面,但当前浏览器自动化 URL 策略拒绝再次操作该标签页。前端入口此前已完成人工可见页面和导航检查,本任务未修改前端输出。 + +## 当前结论 + +JSON Store、原子写入、缺失文件初始化和演示数据已完成;后端数据目录固定在 `server/data/`,不会随启动目录变化。 diff --git a/server/src/data/files.ts b/server/src/data/files.ts new file mode 100644 index 0000000..e5f08d8 --- /dev/null +++ b/server/src/data/files.ts @@ -0,0 +1,17 @@ +import { join } from 'node:path' +import { dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const serverDirectory = join(dirname(fileURLToPath(import.meta.url)), '..', '..') +export const dataDirectory = process.env.DATA_DIR ?? join(serverDirectory, 'data') +export const dataFiles = { + profile: 'profile.json', + settings: 'settings.json', + questions: 'questions.json', + practiceSessions: 'practice-sessions.json', + practiceRecords: 'practice-records.json', + wrongQuestions: 'wrong-questions.json', + studyPlans: 'study-plans.json', + mockExams: 'mock-exams.json', + news: 'news.json' +} as const diff --git a/server/src/data/seed.ts b/server/src/data/seed.ts new file mode 100644 index 0000000..9832bac --- /dev/null +++ b/server/src/data/seed.ts @@ -0,0 +1,50 @@ +import { dataFiles } from './files.js' +import { readData, writeData } from './store.js' +import type { NewsItem, Profile, Question, StudyPlan } from '../types/index.js' + +const questions: Question[] = [ + { + id: 'demo-q-001', type: '行测', module: '数量关系', subModule: '行程问题', difficulty: '中等', + stem: '甲、乙两车分别从 A、B 两地同时出发相向而行,甲车速度为 60km/h,乙车速度为 40km/h,两车相遇后甲车又行驶 2 小时到达 B 地,问 A、B 两地相距多少千米?', + options: [{ key: 'A', text: '240 千米' }, { key: 'B', text: '260 千米' }, { key: 'C', text: '280 千米' }, { key: 'D', text: '300 千米' }], answer: 'C', + analysis: '相遇后甲车行驶 60×2=120 千米,此路程等于乙车相遇前行驶的路程。由速度比可得相遇时间为 3 小时,两地距离为 100×3=300 千米。', tags: ['相遇问题'], source: '内置演示题库', createdAt: '2026-08-27T00:00:00+08:00' + }, + { + id: 'demo-q-002', type: '行测', module: '言语理解', subModule: '逻辑填空', difficulty: '简单', + stem: '在公共治理中,既要善于运用法治思维,也要避免治理方式过于____,让政策执行更有温度。', + options: [{ key: 'A', text: '机械' }, { key: 'B', text: '灵活' }, { key: 'C', text: '主动' }, { key: 'D', text: '积极' }], answer: 'A', + analysis: '根据“避免”与“更有温度”的语境,应选择表示僵化、不知变通的“机械”。', tags: ['语境分析'], source: '内置演示题库', createdAt: '2026-08-27T00:00:00+08:00' + }, + { + id: 'demo-q-003', type: '行测', module: '判断推理', subModule: '图形推理', difficulty: '中等', + stem: '下列图形规律题中,若每次顺时针旋转 90°,第三个图形应如何变化?', + options: [{ key: 'A', text: '保持不变' }, { key: 'B', text: '顺时针旋转 90°' }, { key: 'C', text: '逆时针旋转 90°' }, { key: 'D', text: '翻转' }], answer: 'B', + analysis: '观察相邻图形可知,每一步均按顺时针方向旋转 90°。', tags: ['位置变化'], source: '内置演示题库', createdAt: '2026-08-27T00:00:00+08:00' + } +] + +const profile: Profile = { nickname: '备考人', examDate: '2026-11-30', targetScore: 72, startedAt: '2026-06-01' } +const plans: StudyPlan[] = [ + { id: 'plan-demo-001', date: '2026-08-27', title: '言语理解专项', type: '刷题', target: '完成 20 题', status: 'done' }, + { id: 'plan-demo-002', date: '2026-08-27', title: '数量关系专项', type: '刷题', target: '完成 15 题', status: 'done' }, + { id: 'plan-demo-003', date: '2026-08-27', title: '错题复盘', type: '复习', target: '复习 8 道', status: 'pending' } +] +const news: NewsItem[] = [{ id: 'news-demo-001', title: '聚焦高质量发展,把握时代脉搏', category: '政治', summary: '持续推动高质量发展,为中国式现代化夯实基础。', content: '## 今日要闻\n\n高质量发展是全面建设社会主义现代化国家的首要任务。\n\n> 可用于申论积累:发展必须坚持以人民为中心。', source: '新华社', publishedAt: '2026-08-27T10:20:00+08:00', tags: ['高质量发展', '申论素材'], importSource: 'json' }] + +export async function initializeData() { + await readData(dataFiles.profile, profile) + await readData(dataFiles.settings, { dailyTargetMinutes: 90, reminderTime: '19:30', preferredDuration: 15 }) + await readData(dataFiles.questions, questions) + await readData(dataFiles.practiceSessions, []) + await readData(dataFiles.practiceRecords, []) + await readData(dataFiles.wrongQuestions, []) + await readData(dataFiles.studyPlans, plans) + await readData(dataFiles.mockExams, []) + await readData(dataFiles.news, news) +} + +export async function resetDemoData() { + await Promise.all([ + writeData(dataFiles.profile, profile), writeData(dataFiles.questions, questions), writeData(dataFiles.studyPlans, plans), writeData(dataFiles.news, news) + ]) +} diff --git a/server/src/data/store.ts b/server/src/data/store.ts new file mode 100644 index 0000000..6dcf9db --- /dev/null +++ b/server/src/data/store.ts @@ -0,0 +1,34 @@ +import { mkdir, readFile, rename, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { dataDirectory } from './files.js' + +export async function ensureDataDirectory() { + await mkdir(dataDirectory, { recursive: true }) +} + +export async function readData(name: string, fallback: T): Promise { + await ensureDataDirectory() + try { + const raw = await readFile(join(dataDirectory, name), 'utf8') + return JSON.parse(raw) as T + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw new Error(`无法读取 ${name}:${String(error)}`) + await writeData(name, fallback) + return fallback + } +} + +export async function writeData(name: string, data: T) { + await ensureDataDirectory() + const target = join(dataDirectory, name) + const temp = `${target}.tmp` + await writeFile(temp, `${JSON.stringify(data, null, 2)}\n`, 'utf8') + await rename(temp, target) +} + +export async function updateData(name: string, fallback: T, updater: (data: T) => T) { + const current = await readData(name, fallback) + const next = updater(current) + await writeData(name, next) + return next +} diff --git a/server/src/server.ts b/server/src/server.ts index 538d41e..89daec9 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,7 +1,9 @@ import Fastify from 'fastify' import cors from '@fastify/cors' +import { initializeData } from './data/seed.js' const app = Fastify({ logger: true }) await app.register(cors, { origin: true }) +await initializeData() app.get('/health', async () => ({ status: 'ok', service: 'gwy-server' })) app.listen({ port: Number(process.env.PORT ?? 3000), host: process.env.HOST ?? '0.0.0.0' }) diff --git a/server/src/types/index.ts b/server/src/types/index.ts new file mode 100644 index 0000000..9179a89 --- /dev/null +++ b/server/src/types/index.ts @@ -0,0 +1,21 @@ +export type QuestionOption = { key: 'A' | 'B' | 'C' | 'D'; text: string } + +export type Question = { + id: string + type: '行测' + module: string + subModule: string + difficulty: '简单' | '中等' | '困难' + stem: string + options: QuestionOption[] + answer: QuestionOption['key'] + analysis: string + tags: string[] + source: string + createdAt: string +} + +export type Profile = { nickname: string; examDate: string; targetScore: number; startedAt: string } +export type StudyPlan = { id: string; date: string; title: string; type: string; target: string; status: 'pending' | 'done' } +export type MockExam = { id: string; name: string; date: string; total: number; modules: Record; note: string } +export type NewsItem = { id: string; title: string; category: string; summary: string; content: string; source: string; publishedAt: string; tags: string[]; importSource: string }