feat: add json data store and demo data
This commit is contained in:
parent
4ccd97254a
commit
b637ea9ef0
21
docs/checks/task-02.md
Normal file
21
docs/checks/task-02.md
Normal 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
17
server/src/data/files.ts
Normal 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
50
server/src/data/seed.ts
Normal 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
34
server/src/data/store.ts
Normal 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
|
||||
}
|
||||
@ -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
21
server/src/types/index.ts
Normal 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 }
|
||||
Loading…
x
Reference in New Issue
Block a user