做 MBTI 测试系统这个念头,最开始其实挺偶然的。我本职是前端出身,平时写 Node.js 只为了搞定一些简单的工具脚本,从来没想过要正儿八经搭一个完整的应用。直到有个朋友的公司要做内部团建,想搞一个“团队性格画像”的小活动,问能不能做成一个网页版的 MBTI 测试,大家扫个码就能答题,答完还能把结果存下来。我随口说了一句“可以用 Node.js + SQLite 搞”,当时只是想显得专业一点,没想到后面真的踩出了一个还挺完整的小项目。
做完之后我复盘了很久,发现这个需求其实非常有代表性:轻量、够用、数据不上云、还要界面好看。今天就以这个实战案例为线索,把从技术选型、数据库设计、后端接口、前端交互到最后的坑位排查,完整地拆给你看。不管你是想练手的初级 Node.js 开发者,还是工作中需要快速交付一个内部工具的同学,这篇文章都值得你花十分钟看完,因为里面每一步都是可以直接复现的。
1. 项目概述与整体思路拆解
1.1 为什么偏偏选 Node.js + SQLite
先回答一个几乎所有的朋友都会问的问题:这年头不都用 MongoDB、MySQL 或者干脆 POSTGRESQL 吗,为什么用 SQLite?
原因分三层。
第一层是需求规模决定了选型。MBTI 测试系统本身就是一个低频小应用,就算给一家 500 人的公司做团建,一天的答题量撑死也就几百条。这种量级的数据,完全没必要引入一台独立的数据库服务。SQLite 是一个嵌入式关系型数据库,说的直白一些,它说白了就是一个文件,你把文件放哪儿,数据就在哪儿。没有服务进程、没有端口、没有权限配置,只要 Node.js 能读文件,它就能读写数据库。
第二层是开发效率。Node.js 配上 sqlite3 这个 npm 包,要做的事非常聚焦——初始化一个 db 文件、建表、写 SQL。不需要像操作 MySQL 那样先安装一个客户端、再申请账号密码、还要考虑字符集编码。整个过程干净利落,特别适合小团队快速出活。
第三层是部署成本几乎为零。最后我打包出去的方式更是简单到了极致:服务器上装一个 Node.js 环境,把项目文件夹一拷,然后 node server.js 一跑就上线了。不需要配 Nginx 反向代理(当然配了更规范),不需要管数据库备份策略,每天定时把那个 .db 文件复制走就是一份完整的备份。这一点在很多内部小工具里是巨大的优势。
所以 Node.js + SQLite 这套组合,玩的就是一个“轻快省事”。如果你在做的是一个重数据分析、高并发、多机部署的项目,那它确实不够看;但如果你面临的需求是“小团队内部快速交付、要能跑还要能看”,这套组合绝对是最合适的起点。
1.2 功能需求拆解与页面流转设计
做任何小项目,最怕的就是不提需求直接上手写代码。我当时把需求粗略梳理成了三条线,这也成为后来功能开发的骨架:
- 答题线:用户打开网页 → 看到测试介绍页 → 点击开始答题 → 一道一道往下选 → 提交答案。
- 判定线:服务端接收答案数组 → 计算四个维度的得分 → 得出 MBTI 类型(如 INTP、ENFJ)→ 返回结果。
- 管理线:将每一条测试记录写入 SQLite,包括答题明细、结论类型、提交时间,方便之后做简单的统计分析。
为了避免页面来回跳转带来的割裂感,我把整体设计定成了单页应用 + 服务端 API的模式。前端用原生 HTML/CSS/JavaScript,负责渲染题目和收集答案;后端用 Express 提供两个核心接口——一个承接题目列表,一个接收并判定答案。这样前后端职责分离,逻辑也清晰:前端只管“展示与收集”,后端管“算法与存储”。
这里有一个很重要的设计心得分享给你:题目数据千万不要硬编码在前端 JS 里。刚开始我图省事,把所有题目直接写成一个数组放页面顶部,结果每次修改一道题都要去翻前端代码,还要冒着改错格式的风险。后来我把题目全部塞进 SQLite 表里,前端请求 /api/questions 动态获取。这样一来,换题目只需要更新数据库,不需要动一行前端代码,体验完全不一样。数据与视图分离,哪怕是一个小项目,也值得在早期就养成这个习惯。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与基础工具链
2.1 Node.js 环境安装与验证
开始写业务代码之前,先把 Node.js 环境理清楚。
以我现在用的 18.20.4 LTS 版本为例,直接去官网下载安装包,一顿点击“下一步”就好了。装完之后,需要验证环境是否正常,打开终端分别执行以下两条命令:
bash复制node -v
npm -v
如果终端输出类似 v18.20.4 和 10.7.0 的版本号,说明安装成功。这里要特别提醒一句:新手经常会犯的一个错误是装完了 Node.js 就忘了装 npm,或者以为装好了其实 PATH 没生效,结果执行命令报 node: command not found。遇到这种情况,关掉当前终端窗口重新开一个再试,通常就能解决;如果还不行,检查一下环境变量 PATH 里有没有 Node.js 的安装目录。
项目在哪个目录下建都行,我习惯建一个干净的目录,名字就叫 mbti-test。然后初始化项目:
bash复制mkdir mbti-test
cd mbti-test
npm init -y
npm init -y 会快速生成一个 package.json。接着安装项目依赖:
bash复制npm install express sqlite3 cors
- express:Node.js 后端最流行的 Web 框架,路由和中间件机制都很成熟;
- sqlite3:SQLite 的 Node.js 驱动包,负责建库建表、执行 SQL;
- cors:处理跨域请求。如果你的前端页面和后端服务的端口不一样,比如前端开在 3000、后端开在 5500,那这个包几乎是必需的,否则浏览器控制台会给你刷一屏 CORS 报错。
这里想多说一句关于 sqlite3 的安装。有些环境在 npm install 的时候会卡在编译原生模块这一步,原因是它需要下载预编译二进制文件。如果网络不给力,可以试试设置 npm 镜像源之后重新安装:
bash复制npm config set registry https://registry.npmmirror.com
npm install sqlite3 --build-from-source
实测下来,设置国内镜像源后安装速度会有明显改善。这个坑很隐蔽,很多人装了大半天以为是自己代码的问题,其实是安装源的问题。
2.2 SQLite 数据库与可视化工具准备
SQLite 本身是一个库文件,不像 MySQL 那样有一个独立的服务需要启动。但开发过程中要想直观地“看到”数据库里的内容,光靠命令行敲 sqlite3 进交互模式还是太折磨了。我强烈推荐装一个桌面可视化工具——DB Browser for SQLite。
这个工具是免费的,跨 Windows、macOS、Linux 都能用。它最大的价值在于三块:
- 可视化建表:不用手写建表语句,填几个字段、点一下保存就生成一张表;
- 直接查数据:双击打开
.db文件,切到“浏览数据”面板,所有记录以表格形式呈现; - 执行自定义 SQL:如果想跑一条复杂的关联查询,直接在“执行 SQL”标签页里写就好,相当于白送一个轻量级数据库客户端。
我整个项目调试期间几乎都用它来验证数据写入是否成功、类型计算结果是否符合预期。可以这么说:没有 DB Browser for SQLite,调试效率至少减半。它是你在每一个 Node.js + SQLite 项目里都值得花 10 分钟装上的小工具。
3. 数据库设计与题目数据准备
3.1 MBTI 题目表的字段设计
MBTI 的底层逻辑是四组对立的性格维度:E/I(外向与内向)、S/N(感觉与直觉)、T/F(思考与情感)、J/P(判断与感知)。每个人的测试结果就是从每个维度里取其中一个字母,四个字母拼起来就是性格类型,比如 INTP、ESTJ、INFJ 这类。
为了能让系统计算类型,题目表的设计必须能让程序很快判断“用户选了某个选项,给哪个维度加分”。我把题目表 questions 设计成了这样:
sql复制CREATE TABLE IF NOT EXISTS questions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dimension TEXT NOT NULL,
question TEXT NOT NULL,
option_a TEXT NOT NULL,
option_b TEXT NOT NULL,
tag_a TEXT NOT NULL,
tag_b TEXT NOT NULL
);
解释一下字段含义:
dimension:当前题目所属的维度,取值是EI、SN、TF、JP之一;option_a和option_b:这是用户在页面上看到的两个选项文字;tag_a和tag_b:选项被选中的时候,给哪一个性格极加点。比如一道EI维度的题,tag_a是E,tag_b是I,那用户选了 A,就在 E 的总分上加 1,选了 B 就在 I 的总分上加 1。
这种设计把“展示文案”和“计分逻辑”解耦了,后续换题目、换选项,对计分代码零影响。
数据库里还需要一张记录表,保存每一次测试的明细:
sql复制CREATE TABLE IF NOT EXISTS test_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
answer_json TEXT NOT NULL,
result_type TEXT NOT NULL,
result_desc TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
answer_json 字段存的是用户答案的原始 JSON 字符串,相当于一个备份数据——万一以后要重新分析答题行为,可以拿它来做数据挖掘;result_type 存计算得出的四个字母;result_desc 可以放一段性格描述文案。
3.2 题目与选项的 INSERT 脚本编写
建表之后就是灌数据。你可以在 DB Browser for SQLite 里手动一行行插入,但更实用的是写一段 SQL 脚本一次性导入。我选择了一个比较“偷懒”的方法:启动 Node.js 服务时,检测到 questions 表是空的,就自动执行插入。
这里的关键不是怎么插,而是题目的内容设计。MBTI 题目不是随口编的,每一道题都要能有效区分对应的维度。我整理了一部分实战可用的题,你会发现它们的结构高度一致,都是“两个对立的情境描述”:
sql复制INSERT INTO questions (dimension, question, option_a, option_b, tag_a, tag_b) VALUES
('EI', '周末受邀参加一个热闹的聚会,你的第一反应是什么?', '充满期待, 觉得放松开心', '想推掉, 更喜欢安静做自己的事', 'E', 'I'),
('EI', '到了一个陌生环境,你更愿意怎么做?', '主动和别人搭话, 快速融入', '先观察旁观, 找到熟悉的人再开口', 'E', 'I'),
('SN', '接手一个新任务时,你更关注什么?', '落实情况、具体步骤、可执行的细节', '整体方向、未来潜力和各种可能性', 'S', 'N'),
('SN', '回忆一次旅行时,你印象最深的是哪种内容?', '做过的具体事情、吃过的食物、走过的路线', '当时的整体氛围、触发的情感和想象', 'S', 'N'),
('TF', '朋友跟你倾诉烦恼时,你觉得更合适的反应是?', '分析问题原因, 给出解决思路', '先安抚情绪, 肯定他的感受', 'T', 'F'),
('TF', '做重大决定时,你更信赖哪种依据?', '逻辑分析、数据对比、客观事实', '内心感受、他人感受、关系和谐度', 'T', 'F'),
('JP', '工作计划遇到中途变动,你会怎么应对?', '按原计划推进, 尽量排除干扰', '灵活调整安排, 拥抱突发变化', 'J', 'P'),
('JP', '你更习惯哪种生活节奏?', '提前规划, 按清单逐项完成', '随兴行动, 根据当下心情安排', 'J', 'P');
每条题目都需要围绕它所在的维度设计,避免出现“选这个既能加 E 又能加 J”的模糊情况。比如一道题,问的是“你会怎么准备旅行”,如果选项写“提前订好所有酒店”和“出发了再说”,它测的就是 J/P 维度;如果选项写“通知大家日期”和“自己悄悄攻略”,那就不够纯粹了。题目纯度是直接影响测试准确率的关键,这也是整个项目里我不会妥协的一点。
4. 后端服务与核心计分算法实现
4.1 Express 初始化与 API 接口编写
项目后端核心文件是 server.js。从零起步的初始化代码如下:
javascript复制const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const cors = require('cors');
const path = require('path');
const app = express();
const PORT = 5500;
// 中间件
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
// 连接 SQLite 数据库
const db = new sqlite3.Database('./mbti.db', (err) => {
if (err) {
console.error('数据库连接失败:', err.message);
} else {
console.log('数据库连接成功');
initDatabase();
}
});
function initDatabase() {
db.run(
`CREATE TABLE IF NOT EXISTS questions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dimension TEXT NOT NULL,
question TEXT NOT NULL,
option_a TEXT NOT NULL,
option_b TEXT NOT NULL,
tag_a TEXT NOT NULL,
tag_b TEXT NOT NULL
)`,
insertSeedQuestions
);
}
function insertSeedQuestions() {
db.get('SELECT COUNT(*) AS count FROM questions', (err, row) => {
if (err) return console.error(err);
if (row.count > 0) return;
// 执行 INSERT 语句...
console.log('初始化题目数据完成');
});
}
app.listen(PORT, () => {
console.log(`服务已启动: http://localhost:${PORT}`);
});
到这里,一个问题值得认真想想:为什么需要通过 express.static 同时托管前端页面? 其实这样做是故意为之——把前端文件放进 public 文件夹,访问 http://localhost:5500 就能直接看到页面,无需另起一个静态服务器,也不用考虑跨域。当然,如果你坚持前后端分离部署,也保留刚才装的 cors,两条路都通。对小项目而言,合在一起部署是最省心的。
紧接着写两个接口。第一个是获取全部题目:
javascript复制// 获取全部测试题目
app.get('/api/questions', (req, res) => {
const sql = 'SELECT id, dimension, question, option_a, option_b FROM questions ORDER BY id';
db.all(sql, [], (err, rows) => {
if (err) {
res.status(500).json({ code: 1, msg: err.message });
} else {
res.json({ code: 0, data: rows });
}
});
});
第二个是提交答案并返回测试结果:
javascript复制// 提交答案并判定类型
app.post('/api/submit', (req, res) => {
const answers = req.body.answers; // 形如 [{ questionId: 1, choice: 'A' }, ...]
if (!Array.isArray(answers) || answers.length === 0) {
return res.status(400).json({ code: 1, msg: '答案不能为空' });
}
// 汇总四个维度的得分
const dimensionScores = { E: 0, I: 0, S: 0, N: 0, T: 0, F: 0, J: 0, P: 0 };
// 这里不能逐题查数据库,否则 N 个查询请求会把串行执行的 sqlite 打满
const sql = 'SELECT dimension, option_a, option_b, tag_a, tag_b FROM questions WHERE id = ?';
// 数据库串行遍历
...
});
实际代码里我用了 async 配合 db.all(...) 成批查询,把题目一次性取出来,再在后端内存中遍历计分,这样避免了每道题单独查一次数据库的性能开销。用批量查询替代循环查询,这是后端性能建模的第一个好习惯。
最终计分函数的实现在前面提炼成独立模块更有复用价值,我把它放进 utils/mbti.js 里:
javascript复制function calculateMBTI(questionRows, answersMap) {
const scores = { E: 0, I: 0, S: 0, N: 0, T: 0, F: 0, J: 0, P: 0 };
for (const row of questionRows) {
const answer = answersMap[row.id];
if (!answer) continue;
if (answer === 'A') {
scores[row.tag_a] += 1;
} else if (answer === 'B') {
scores[row.tag_b] += 1;
}
}
const type = [
scores.E >= scores.I ? 'E' : 'I',
scores.S >= scores.N ? 'S' : 'N',
scores.T >= scores.F ? 'T' : 'F',
scores.J >= scores.P ? 'J' : 'P',
].join('');
return {
type,
scores,
};
}
这套算法的核心逻辑是二分计分法:同一维度内,比较两个相反字母的累计得分,谁高最终取谁。遇到相等的情况,统一偏向于取前一个字母(比如 E 和 I 等分时取 E),这种处理方式在大多数 MBTI 在线测试工具中也是常见的,毕竟“刚好各占一半”的概率很低,没必要为它单独扩展规则。
计算出结果之后,还需要把记录落库。这里用一个 result_desc 字段描述每种类型对应的“一句话人设”,这部分文案我放在后端字典里,方便以后维护。
4.2 DB 写入与异步控制细节
sqlite3 默认是异步回调风格的,它的 db.run / db.all 都不返回 Promise。在 submit 接口中,如果先用 db.all 查询题目,再 db.run 插入记录,两个操作有先后依赖关系,很容易踩 “回调地狱”的坑。
我直接用了 Node.js 原生 util.promisify 把方法包装成 Promise 风格,代码可读性和可维护性好很多:
javascript复制const { promisify } = require('util');
const dbAll = promisify(db.all).bind(db);
const dbRun = promisify(db.run).bind(db);
// 示例
const rows = await dbAll(sql, []);
这里有个不能忽视的细节:SQLite 一次只能有一个写操作。如果某个瞬间来了多个并发提交,sqlite3 批次会依次排队执行,所以不会出现数据损坏。但性能上如果不注意,可能在同一时间发起大量并发查询,回调队列堆积导致接口响应变慢。这才是我上文用“批量取题目,内存里计算”的根本原因。对 SQLite 而言,能用一条 SQL 解决问题,就不要用 N 条 SQL。
下一步,在 submit 接口里完成计算结果的落库:
javascript复制const answerJson = JSON.stringify(answers);
const insertSql = 'INSERT INTO test_records (answer_json, result_type, result_desc) VALUES (?, ?, ?)';
const desc = typeDescriptions[type] || '未知类型';
await dbRun(insertSql, [answerJson, type, desc]);
res.json({
code: 0,
data: {
type,
desc,
scores: dimensionScores,
},
});
至此,整个后端的基本闭环已经完成:接题、算分、入库、返回结果。
5. “高颜值” 前端交互页面实现
5.1 页面结构与视觉风格设计
“高颜值”三个字是这个项目的核心卖点之一。市面上绝大多数 MBTI 工具界面,风格不是花里胡哨就是老旧过时。我定的视觉基调是:温柔、轻量、有呼吸感,主色调定成低饱和度的莫兰迪蓝绿,背景用淡淡的渐变色,内容区采用毛玻璃质感卡片,整体的沉浸感远超普通白底页面。
页面文件放在 public 目录下,文件结构如下:
code复制public/
index.html
style.css
app.js
assets/
result-bg.jpg
index.html 只有三个关键区块:
- 欢迎区:标题 + 简短介绍 + “开始测试”按钮;
- 答题区:题目卡片 + 两个选项卡片,选项卡片支持选中高亮;
- 结果区:展示 MBTI 四个字母、性格描述、以及一个“重新测试”按钮。
欢迎区的核心 HTML 骨架长这样:
html复制<section id="hero">
<h1>找到你的性格密码</h1>
<p>基于 MBTI 理论的 8 道情景测试,约 2 分钟完成</p>
<button id="startBtn" class="primary-btn">开始测试</button>
</section>
我把答题区做成一个隐藏容器,点击“开始测试”时通过 JavaScript 切换显示,整个过程是同一页内的状态切换,没有整页刷新。这种 SPA 式的体验在小页面里特别显流畅,切换也不会有白屏闪烁。
5.2 题目渲染与计分提交的前端逻辑
app.js 的核心逻辑围绕三个阶段展开:加载题目、渲染一道题、收集选择并渲染下一道。
题目加载通过 fetch 实现:
javascript复制async function loadQuestions() {
const res = await fetch('/api/questions');
const json = await res.json();
if (json.code === 0) {
questions = json.data;
showQuestion(0);
}
}
渲染一道题时,要把 option_a 和 option_b 动态填充到两个卡片按钮上。这里我特意加了一个小细节——每次切换题目时给卡片重新加一个淡入动画,视觉上显得有设计感,而几乎零成本实现:
javascript复制function showQuestion(index) {
if (index >= questions.length) {
submitAnswers();
return;
}
const q = questions[index];
// 渲染题目
$('#questionCount').text(`第 ${index + 1} / ${questions.length} 题`);
$('#questionText').text(q.question);
$('#optionA').text(q.option_a);
$('#optionB').text(q.option_b);
// 清空选中状态
$('.option-card').removeClass('selected');
// 点击绑定
window.currentIndex = index;
}
function chooseOption(tag) {
answers.push({
questionId: questions[window.currentIndex].id,
choice: tag, // 'A' 或 'B'
});
showQuestion(window.currentIndex + 1);
}
这里有个很关键却容易被忽略的交互细节:选完当前题目之后,下一道题应该是马上滑入而不是等用户再按一次“下一题”按钮。项目里我直接用点击选项触发跳转,这样答题节奏非常紧凑,用户连续点选的沉浸感强。实际测试后,弃测率明显降低。
所有题目答完后,前端把答案数组 POST 给 /api/submit:
javascript复制async function submitAnswers() {
const res = await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ answers }),
});
const json = await res.json();
if (json.code === 0) {
showResult(json.data);
}
}
要是接口报错,页面不能只干巴巴地弹一个 alert,我做了统一的错误提示条,并把用户已答的答案缓存到 sessionStorage,刷新页面之后可以自动恢复到上一题。这个小功能是后期加的,来自于一次真实的“手滑刷新全丢”事故——从那以后我所有表单类页面都会顺手存一下 sessionStorage。
5.3 结果展示与性格描述的美化技巧
结果页是最容易让用户觉得“这个系统好专业”的地方。我采用了四个大字母的展示方式,每个字母单独一个卡片,底色根据字母不同而不同——E 用暖橙色、I 用冷淡蓝、S 用草绿、N 用紫色、T 用砖红、F 用粉色、J 用深蓝、P 用明黄。这样一屏展示下来,结果页天生就长得很“有数据感”。
结果描述不能只有干巴巴的一行字。我参考了几种性格测试的文案风格,给每个类型都配了一段简短的描述和关键词标签。比如 INTP 的描述是:
“逻辑是你最好的武器,也是你最厚的防御。你喜欢独处,热爱概念推演,在别人看来枯燥的问题,在你眼里是一盘永远下不完的棋。”
再配合几个标签:“理性”“独立”“好奇”“爱钻研”。页面底部放一个“重新测试”按钮,4 个字母以外的数据(如分数明细),我用一个小卡片折叠展示,想看细节的人点开即可。整体的信息密度适中,看结果的人既有仪式感,也不会被满屏数据淹到。
6. 实操中的高频问题与避坑指南
6.1 数据库打开失败与文件路径问题
这个项目开发期间,我撞上过不少坑,随手记录一下那些代价最惨的。第一个坑出现在数据库文件路径。开发时数据库文件在项目根目录 ./mbti.db,后来我用 pm2 部署到服务器上,启动命令的 cwd 不一样,导致 ./mbti.db 的实际路径指向了别的地方,接口一直报“SQLITE_CANTOPEN”。最后排查发现,sqlite3.Database 里的路径是相对进程当前工作目录的,不是相对 JS 文件所在的目录。解法很简单,改用 path.join(__dirname, 'mbti.db') 这种绝对定位方式,一切恢复正常:
javascript复制const dbPath = path.join(__dirname, 'mbti.db');
const db = new sqlite3.Database(dbPath);
第二个坑是“表结构变更但不生效”。我中途给 test_records 增加了一个 result_desc 字段,改完代码重启服务后,插入操作还是报“no such column: result_desc”。原因是数据库文件里早就存在了旧表,CREATE TABLE IF NOT EXISTS 不会对已经存在的表做任何修改。当时我手动进 DB Browser 删掉旧表重新生成,才恢复正常。本着严谨的做法,后来我把建表语句的状态管理做成了“先 DROP TABLE IF EXISTS 再 CREATE TABLE”,仅用于开发环境,生产环境保持不变。
6.2 计分不准与数据类型疏忽
第三个坑来自前端传参。最开始我设计的前端提交参数是这样的:
json复制{ "questionId": 1, "choice": "A" }
而后端的 answersMap 关联字段是用 row.id 取的。问题出在 SQLite 的 id 是 INTEGER 类型,JSON 里的 questionId 如果被 JS 隐式转换成了字符串 "1",再去 answersMap[row.id] 查找就会返回 undefined,计分全部落空。我在本地调试时用 console.log(answersMap) 一眼就发现了,后来在前端封装时统一 Number(questionId) 转成整型,问题彻底消失。
这类“隐式类型不一致”的问题在 JS 生态里防不胜防,尤其是拿数据库主键和外部传入参数做映射的时候,务必加上类型转换或者断言。
第四个坑更隐蔽,也和 sqlite3 的异步特性有关。我最初在 submit 接口里用了这样的写法:
javascript复制db.get('SELECT COUNT(*) FROM questions', (err, row) => {
// 这里面的逻辑执行完之后,又要发起另一个 db 操作
});
一旦代码里有两个 db 操作嵌套在一起,只要第二层回调里报错,错误信息经常被吞掉,接口返回 200 但实际数据没写入。后来全面改用 promisify + async/await 之后,错误可以被 try/catch 捕获到,排查问题心里有底多了。
6.3 一张高频问题对照速查表
把这个项目里最典型的问题整理成表格,方便你以后快速对照:
| 现象 | 可能原因 | 解决建议 |
|---|---|---|
SQLITE_ERROR: no such table |
表还没建或没执行建表脚本 | 检查 initDatabase() 是否被调用,用 DB Browser 查看表是否已生成 |
SQLITE_ERROR: no such column |
旧表结构未更新 | 开发环境可删表重建,生产环境写 ALTER TABLE 迁移 |
| 前端拿到的题目为空数组 | 题目表里没数据 | 手动执行种子 SQL 或检查插入逻辑是否被跳过 |
| 提交结果一直无返回 | 跨域或 JSON 解析失败 | 检查 cors() 是否在路由之前加载,查看浏览器 Network 面板 |
| 计分出来的类型明显与用户预期不符 | 题目纯度不够或答题数据有缺漏 | 先 console.log 打印 answers 和 scores,逐步排查 |
最后一条独家的经验是:生产环境一定要实时备份 .db 文件。这个东西好就好在它只是一个文件,坏也坏在它只是一个文件——如果你哪天磁盘坏了、或者不小心 rm 删错了,数据就真的没了。我用的最简单方案是在服务器上挂个 cron 定时任务,每天把数据库文件复制到另一个盘符,就完成了日常备份。整个项目的复杂度,也恰恰是因为 SQLite 的“文件即数据库”特性,而变得非常可控。
这个项目做到后期,我最大的感受是:技术选型不是越复杂越好,而是越贴合场景越好。Node.js + SQLite 的组合让我用两个依赖包就完成了整条业务闭环,加上原生前端,几十个文件就撑起了一个可以真实运营的内部测试系统。如果你也正好有类似轻量交付的需求,不妨从复制我这份代码骨架开始,把题目换成你自己的,很快就能体验到“一个人带全场”的爽感。
