add flutter quiz project code
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const PORT = process.env.PORT || 3030;
|
||||
const ADMIN_USER = process.env.ADMIN_USER || 'admin';
|
||||
const ADMIN_PASS = process.env.ADMIN_PASS || 'admin123';
|
||||
const DATA_FILE = path.join(__dirname, 'questions.json');
|
||||
|
||||
const app = express();
|
||||
app.use(express.json({ limit: '2mb' }));
|
||||
|
||||
const DIST = path.join(__dirname, 'dist');
|
||||
if (fs.existsSync(DIST)) {
|
||||
app.use(express.static(DIST));
|
||||
app.get(/^\/(?!api\/).*/, (req, res) => {
|
||||
res.sendFile(path.join(DIST, 'index.html'));
|
||||
});
|
||||
} else {
|
||||
console.warn('未找到 dist/ 目录,请先执行 npm run build 再访问页面');
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
if (!fs.existsSync(DATA_FILE)) return { sets: [], nextSetId: 1, nextQId: 1 };
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
|
||||
if (Array.isArray(raw)) {
|
||||
const qs = raw;
|
||||
const migrated = {
|
||||
sets: [{ id: 1, name: 'Dart & Flutter 学习测验', questions: qs }],
|
||||
nextSetId: 2,
|
||||
nextQId: qs.length ? Math.max(...qs.map(q => q.id)) + 1 : 1
|
||||
};
|
||||
fs.writeFileSync(DATA_FILE, JSON.stringify(migrated, null, 2), 'utf8');
|
||||
return migrated;
|
||||
}
|
||||
if (raw && Array.isArray(raw.sets)) return raw;
|
||||
return { sets: [], nextSetId: 1, nextQId: 1 };
|
||||
} catch {
|
||||
return { sets: [], nextSetId: 1, nextQId: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
const data = loadData();
|
||||
|
||||
function saveData() {
|
||||
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
const tokens = new Set();
|
||||
|
||||
function findQuestion(qid) {
|
||||
for (const set of data.sets) {
|
||||
const q = set.questions.find(x => x.id === qid);
|
||||
if (q) return { set, q };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function toPublic(q) {
|
||||
return { id: q.id, lv: q.lv, q: q.q, opts: q.opts };
|
||||
}
|
||||
|
||||
function validateBody(body) {
|
||||
const { lv, q, opts, ans, exp } = body || {};
|
||||
if (!q || typeof q !== 'string' || q.trim() === '') return '题目内容不能为空';
|
||||
if (!Array.isArray(opts) || opts.length < 2) return '至少需要 2 个选项';
|
||||
if (opts.some(o => typeof o !== 'string' || o.trim() === '')) return '选项不能为空';
|
||||
if (typeof ans !== 'number' || !Number.isInteger(ans) || ans < 0 || ans >= opts.length) return '答案序号不正确';
|
||||
if (lv !== undefined && (!Number.isInteger(lv) || lv < 1 || lv > 6)) return '难度需为 1-6 的整数';
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildQuestion(body) {
|
||||
const { lv, q, opts, ans, exp } = body;
|
||||
return { id: data.nextQId++, lv: lv === undefined ? 1 : lv, q: q.trim(), opts, ans, exp: (exp || '').trim() };
|
||||
}
|
||||
|
||||
function validateLvNames(v) {
|
||||
if (v === undefined || v === null) return null;
|
||||
if (!Array.isArray(v) || v.length !== 6 || v.some(x => typeof x !== 'string' || !x.trim())) {
|
||||
return '难度名称必须为 6 个非空字符串';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
app.get('/api/sets', (req, res) => {
|
||||
res.json({ sets: data.sets.map(s => ({ id: s.id, name: s.name, count: s.questions.length, lvNames: s.lvNames || null })) });
|
||||
});
|
||||
|
||||
app.get('/api/questions', (req, res) => {
|
||||
const setId = req.query.set ? Number(req.query.set) : (data.sets[0] ? data.sets[0].id : null);
|
||||
const set = data.sets.find(s => s.id === setId);
|
||||
if (!set) return res.status(404).json({ error: '套题不存在' });
|
||||
res.json({
|
||||
set: { id: set.id, name: set.name, lvNames: set.lvNames || null },
|
||||
count: set.questions.length,
|
||||
questions: set.questions.map(toPublic)
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/check', (req, res) => {
|
||||
const { id, answer } = req.body || {};
|
||||
const hit = findQuestion(id);
|
||||
if (!hit) return res.status(404).json({ error: '题目不存在' });
|
||||
if (!Number.isInteger(answer)) return res.status(400).json({ error: 'answer 必须为数字' });
|
||||
res.json({ correct: hit.q.ans === answer, answer: hit.q.ans, exp: hit.q.exp });
|
||||
});
|
||||
|
||||
app.post('/api/admin/login', (req, res) => {
|
||||
const { username, password } = req.body || {};
|
||||
if (username === ADMIN_USER && password === ADMIN_PASS) {
|
||||
const token = crypto.randomBytes(24).toString('hex');
|
||||
tokens.add(token);
|
||||
res.json({ token });
|
||||
} else {
|
||||
res.status(401).json({ error: '用户名或密码错误' });
|
||||
}
|
||||
});
|
||||
|
||||
function requireAuth(req, res, next) {
|
||||
const token = (req.headers.authorization || '').replace(/^Bearer\s+/i, '');
|
||||
if (token && tokens.has(token)) return next();
|
||||
res.status(401).json({ error: '未登录或登录已过期' });
|
||||
}
|
||||
|
||||
app.get('/api/admin/sets', requireAuth, (req, res) => {
|
||||
res.json({ sets: data.sets });
|
||||
});
|
||||
|
||||
app.post('/api/admin/sets', requireAuth, (req, res) => {
|
||||
const { name } = req.body || {};
|
||||
if (!name || typeof name !== 'string' || name.trim() === '') return res.status(400).json({ error: '套题名称不能为空' });
|
||||
const lvErr = validateLvNames(req.body.lvNames);
|
||||
if (lvErr) return res.status(400).json({ error: lvErr });
|
||||
const set = { id: data.nextSetId++, name: name.trim(), questions: [] };
|
||||
if (req.body.lvNames !== undefined) set.lvNames = req.body.lvNames;
|
||||
data.sets.push(set);
|
||||
saveData();
|
||||
res.json(set);
|
||||
});
|
||||
|
||||
app.put('/api/admin/sets/:id', requireAuth, (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const set = data.sets.find(s => s.id === id);
|
||||
if (!set) return res.status(404).json({ error: '套题不存在' });
|
||||
const { name } = req.body || {};
|
||||
if (!name || typeof name !== 'string' || name.trim() === '') return res.status(400).json({ error: '套题名称不能为空' });
|
||||
const lvErr = validateLvNames(req.body.lvNames);
|
||||
if (lvErr) return res.status(400).json({ error: lvErr });
|
||||
set.name = name.trim();
|
||||
if (req.body.lvNames !== undefined) set.lvNames = req.body.lvNames;
|
||||
saveData();
|
||||
res.json(set);
|
||||
});
|
||||
|
||||
app.delete('/api/admin/sets/:id', requireAuth, (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const idx = data.sets.findIndex(s => s.id === id);
|
||||
if (idx === -1) return res.status(404).json({ error: '套题不存在' });
|
||||
data.sets.splice(idx, 1);
|
||||
saveData();
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.post('/api/admin/import', requireAuth, (req, res) => {
|
||||
const { name, questions } = req.body || {};
|
||||
if (!name || typeof name !== 'string' || name.trim() === '') return res.status(400).json({ error: '套题名称不能为空' });
|
||||
if (!Array.isArray(questions)) return res.status(400).json({ error: 'questions 必须为数组' });
|
||||
const lvErr = validateLvNames(req.body.lvNames);
|
||||
if (lvErr) return res.status(400).json({ error: lvErr });
|
||||
for (let i = 0; i < questions.length; i++) {
|
||||
const err = validateBody(questions[i]);
|
||||
if (err) return res.status(400).json({ error: '第 ' + (i + 1) + ' 题格式错误:' + err });
|
||||
}
|
||||
const set = { id: data.nextSetId++, name: name.trim(), questions: questions.map(buildQuestion) };
|
||||
if (req.body.lvNames !== undefined) set.lvNames = req.body.lvNames;
|
||||
data.sets.push(set);
|
||||
saveData();
|
||||
res.json({ set: { id: set.id, name: set.name, count: set.questions.length }, imported: set.questions.length });
|
||||
});
|
||||
|
||||
app.get('/api/admin/sets/:id/export', requireAuth, (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const set = data.sets.find(s => s.id === id);
|
||||
if (!set) return res.status(404).json({ error: '套题不存在' });
|
||||
res.json({
|
||||
name: set.name,
|
||||
lvNames: set.lvNames || null,
|
||||
exportedAt: new Date().toISOString(),
|
||||
questions: set.questions.map(q => ({ lv: q.lv, q: q.q, opts: q.opts, ans: q.ans, exp: q.exp }))
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/admin/sets/:id/questions', requireAuth, (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const set = data.sets.find(s => s.id === id);
|
||||
if (!set) return res.status(404).json({ error: '套题不存在' });
|
||||
const err = validateBody(req.body);
|
||||
if (err) return res.status(400).json({ error: err });
|
||||
const item = buildQuestion(req.body);
|
||||
set.questions.push(item);
|
||||
saveData();
|
||||
res.json(item);
|
||||
});
|
||||
|
||||
app.put('/api/admin/questions/:qid', requireAuth, (req, res) => {
|
||||
const hit = findQuestion(Number(req.params.qid));
|
||||
if (!hit) return res.status(404).json({ error: '题目不存在' });
|
||||
const err = validateBody(req.body);
|
||||
if (err) return res.status(400).json({ error: err });
|
||||
const { lv, q, opts, ans, exp } = req.body;
|
||||
const item = hit.q;
|
||||
item.lv = lv === undefined ? item.lv : lv;
|
||||
item.q = q.trim();
|
||||
item.opts = opts;
|
||||
item.ans = ans;
|
||||
item.exp = (exp || '').trim();
|
||||
saveData();
|
||||
res.json(item);
|
||||
});
|
||||
|
||||
app.delete('/api/admin/questions/:qid', requireAuth, (req, res) => {
|
||||
const hit = findQuestion(Number(req.params.qid));
|
||||
if (!hit) return res.status(404).json({ error: '题目不存在' });
|
||||
const idx = hit.set.questions.indexOf(hit.q);
|
||||
hit.set.questions.splice(idx, 1);
|
||||
saveData();
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log('测验系统已启动: http://localhost:' + PORT);
|
||||
console.log('答题页: http://localhost:' + PORT + '/');
|
||||
console.log('管理后台: http://localhost:' + PORT + '/admin.html');
|
||||
console.log('管理员账号: ' + ADMIN_USER + ' / ' + ADMIN_PASS);
|
||||
});
|
||||
Reference in New Issue
Block a user