feat: add json data store and demo data

This commit is contained in:
liyy 2026-08-28 11:57:21 +08:00
parent 4ccd97254a
commit b637ea9ef0
6 changed files with 145 additions and 0 deletions

21
docs/checks/task-02.md Normal file
View File

@ -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/`,不会随启动目录变化。

17
server/src/data/files.ts Normal file
View File

@ -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

50
server/src/data/seed.ts Normal file
View File

@ -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<Profile>(dataFiles.profile, profile)
await readData(dataFiles.settings, { dailyTargetMinutes: 90, reminderTime: '19:30', preferredDuration: 15 })
await readData<Question[]>(dataFiles.questions, questions)
await readData(dataFiles.practiceSessions, [])
await readData(dataFiles.practiceRecords, [])
await readData(dataFiles.wrongQuestions, [])
await readData<StudyPlan[]>(dataFiles.studyPlans, plans)
await readData(dataFiles.mockExams, [])
await readData<NewsItem[]>(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)
])
}

34
server/src/data/store.ts Normal file
View File

@ -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<T>(name: string, fallback: T): Promise<T> {
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<T>(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<T>(name: string, fallback: T, updater: (data: T) => T) {
const current = await readData(name, fallback)
const next = updater(current)
await writeData(name, next)
return next
}

View File

@ -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' })

21
server/src/types/index.ts Normal file
View File

@ -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<string, number>; note: string }
export type NewsItem = { id: string; title: string; category: string; summary: string; content: string; source: string; publishedAt: string; tags: string[]; importSource: string }