搞服务端开发这些年,我见过太多新手卡在“代码能跑”和“真正能上线”之间。写接口谁都会,但一套服务端从立项、建模、开发、测试到部署运维,完整的流程走下来,中间全是细节坑。“小智服务端全流程教程”这个标题看着简单,实际恰恰是很多人最需要的东西——它要覆盖的是一整个生命周期,而不只是某个框架的 CRUD。
这篇文章就按我实际带项目的思路来写,把“小智服务端”从零到线上跑的完整链路拆开,跟着做一遍,你能彻底搞懂一个服务端项目是怎么从空目录变成可用系统的。适合刚接触后端、想系统梳理全流程的朋友,也适合那些已经会写接口但没自己部署过、对日志和监控感到陌生的开发者。代码我尽量用最直白的方式给,原理讲透,坑也提前排好。
1. 服务端项目设计:动手写代码前先把这四件事想明白
很多新手拿到需求第一反应是建文件夹、装框架、开写,结果写一半发现目录乱成一团、接口设计前后矛盾、数据库字段改来改去。真正的项目流程第一步不是写代码,而是把设计定清楚。
1.1 先拆需求:这个服务端到底要干哪些活
做“小智服务端”之前,先定义清楚它对外提供什么能力。我的做法是把需求拆成四块清单:
- 核心业务:比如用户注册登录、会话管理、消息收发、状态查询,这是服务端存在的基本价值;
- 账号体系:涉及用户身份识别、鉴权、密码安全,这是几乎所有服务端都绕不开的通用模块;
- 数据持久化:有哪些数据需要落库,数据之间的关系是什么,哪些需要加索引,哪些只允许短期缓存;
- 运维支撑:日志记录、健康检查、错误上报、限流、部署方式。
把这四块写在文档里,哪怕只是几行结构性文字,后面写代码时就不会跑偏。“小智服务端”这个场景下,我当时的拆解结果是先做三个核心模块:用户模块(注册/登录/信息查询)、对话模块(创建会话/发送消息/拉取历史)、管理模块(状态检查/简单统计),每个模块再往下拆接口。这个过程其实是把模糊需求逐步破拆成可执行的开发任务,越早做,后面返工越少。
1.2 技术栈选型:为什么我用 Node.js + Express + SQLite 打底
技术选型没有标准答案,但有一条核心原则——可靠和熟悉比炫技重要。我在“小智服务端”这个项目里选了 Node.js 生态,具体组合是:
- 运行时:Node.js(LTS 版本)
- Web 框架:Express,中间件生态成熟,文档多,踩坑成本低;
- 数据库:SQLite,单文件、零部署,适合中小规模场景,开发期尤其方便;
- 鉴权方案:JWT,无状态、横向扩展简单;
- 测试工具:Jest + supertest;
- 进程管理:PM2,守护进程、日志管理、自动重启全覆盖。
这套组合的直观好处是本地环境依赖极小,不需要装 MySQL、Redis 这类独立服务,新手跑起来的成功率非常高。你可能会问为什么不用 Spring Boot、Django 这类重型框架——不是说它们不好,而是对于“小智服务端”这种轻量级服务,Node.js 的异步模型和前端技术栈的一致性会让你把精力集中在业务逻辑上,而不是被繁重的配置拖住。
当然,如果团队里已经有人对 Python 很熟,也可以用 FastAPI + SQLite,逻辑完全一致,只是框架语法不同。选型最怕的不是选错,而是反复横跳。
1.3 目录结构设计:从一开始就给项目搭好骨架
我见过太多项目把逻辑全堆在 app.js 里,几百行代码一个文件,后面谁都不敢动。合理的目录结构是项目能不能长期维护的基础。我习惯的核心结构长这样:
xiaozhi-server/ src/ server.js # 服务入口,启动 HTTP 服务 app.js # 初始化 Express、挂载中间件与路由 config/ index.js # 配置项集中管理 routes/ # 路由定义 auth.js chat.js controllers/ # 业务逻辑层 authController.js chatController.js services/ # 数据操作与外部服务封装 userService.js messageService.js models/ # 数据库结构定义 index.js middleware/ auth.js errorHandler.js logger.js utils/ response.js jwt.js tests/ auth.test.js chat.test.js .env.example package.json这套结构遵循的是“路由负责分发、控制器负责业务编排、服务层负责数据访问”的分层思想。新手最容易犯的错误是在路由回调里直接写 SQL,短期看省事,一旦接口多了必然失控。分层会牺牲一点初期速度,但换来的是后期改功能时脑子里有清晰的地图。
1.4 环境与配置管理:把敏感信息挡在代码之外
配置管理是全流程里特别容易被忽略、但实际项目中一定会挨打的部分。不要把所有配置写死在代码里。我会用 .env 文件保存端口号、JWT 密钥、数据库路径这类环境相关的变量,代码里统一通过 config/index.js 读取。
// src/config/index.js require('dotenv').config(); module.exports = { port: process.env.PORT || 3000, jwtSecret: process.env.JWT_SECRET || 'dev-secret', jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d', dbPath: process.env.DB_PATH || './data/xiaozhi.db' };这里有一个关键动作:把 .env 文件加入 .gitignore,同时提供一个 .env.example 作为模板提交到仓库,保证团队成员拉代码后能知道需要填哪些变量。密码、API Key 这类东西如果泄露到代码仓库里,后果很严重,别图一时方便。
2. 环境准备与项目初始化:先把第一个接口跑通
这部分的目标很明确:让“小智服务端”能启动、能收到请求、能返回 JSON。就算你完全没搭过 Node.js 项目,按步骤走也能出结果。
2.1 安装运行环境与初始化项目
首先安装 Node.js LTS 版本,装完在终端验证一下:
node -v npm -v能看到版本号说明环境没问题。然后在项目根目录执行:
npm init -y这会生成一个默认的 package.json。接着安装核心依赖:
npm install express dotenv cors npm install --save-dev nodemon- express:Web 框架;
- dotenv:读取 .env 配置文件;
- cors:解决跨域问题,前端页面和本地调试都需要;
- nodemon:开发时监听文件变化自动重启。
2.2 编写入口文件,启动基础 HTTP 服务
我习惯把入口拆成两个文件:app.js 只负责创建应用和挂载中间件,server.js 负责真正监听端口。这样做的好处是后续写接口测试时,可以直接对 app 发请求,不必真正占用端口。
// src/app.js const express = require('express'); const cors = require('cors'); const routes = require('./routes'); const { errorHandler } = require('./middleware/errorHandler'); const app = express(); app.use(cors()); app.use(express.json()); // 健康检查接口,部署后用来确认服务是否存活 app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: Date.now() }); }); app.use('/api', routes); app.use(errorHandler); module.exports = app;// src/server.js const app = require('./app'); const config = require('./config'); app.listen(config.port, () => { console.log(`[xiaozhi-server] listening on port ${config.port}`); });然后在 package.json 的 scripts 里加上启动命令:
"scripts": { "start": "node src/server.js", "dev": "nodemon src/server.js" }这时候运行 npm run dev,访问 http://localhost:3000/health,能看到返回的 JSON,说明服务端基础链路已经通了。
2.3 中间件的加载顺序:为什么它直接影响代码能否正常工作
Express 的中间件是按注册顺序执行的。很多人排查半天问题,最后发现是顺序写错。我的经验是:
- 先注册全局中间件(cors、express.json 这种);
- 再挂载路由;
- 最后挂错误处理中间件。
一个常见的坑是:把错误处理中间件放在路由前面,结果路由抛出的异常根本不会被捕获,直接返回堆栈信息给客户端。错误处理中间件必须放在所有路由之后,因为它的本质是“兜底者”。
3. 数据存储设计:用户和对话记录的建模与落地
服务端没数据就只是空壳。“小智服务端”的核心数据是用户、会话、消息这三类,建模做得好不好直接影响接口好不好写。
3.1 为什么选择 SQLite 而不是直接上 MySQL
选 SQLite 不是因为它“低级”,而是因为它对于这个体量的项目足够合适。SQLite 是一个嵌入式关系型数据库,数据存放在一个本地文件里,不需要单独启动数据库服务,学习成本极低,事务性也完整。
我个人的判断标准是:并发量在个位数万级别、没有复杂的高可用要求、团队小、快速迭代优先,SQLite 完全够用。等以后业务量真的大了,数据库可以平滑迁移到 MySQL,表结构设计的方法是通用的。倒是一开始就折腾 MySQL 集群,半个月过去了还没跑通,心态先崩了。
3.2 表结构设计与建表语句
我设计了三张表:users、conversations、messages。分别对应账号、会话、消息记录。
CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS conversations ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, title TEXT DEFAULT '新会话', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, conversation_id INTEGER NOT NULL, role TEXT NOT NULL CHECK (role IN ('user', 'assistant')), content TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (conversation_id) REFERENCES conversations(id) );这里有两个细节值得注意。第一,密码永远不存明文,存的是哈希后的结果;第二,messages 表里用 conversation_id 外键关联会话,方便一次性拉取某个会话的所有聊天记录。索引方面,后期如果查询变慢,可以对 conversations.user_id 和 messages.conversation_id 加上索引。
Node.js 里我选择用better-sqlite3这个库,它的特点是同步 API、性能好、代码直观,不需要写一堆回调或者 await。在执行建表语句时,可以用代码启动时自动执行:
// src/models/index.js const Database = require('better-sqlite3'); const path = require('path'); const config = require('../config'); const db = new Database(config.dbPath); db.pragma('journal_mode = WAL'); db.exec(` CREATE TABLE IF NOT EXISTS users (...); CREATE TABLE IF NOT EXISTS conversations (...); CREATE TABLE IF NOT EXISTS messages (...); `); module.exports = db;journal_mode = WAL是 SQLite 的一个优化设置,能明显提升并发读写表现,减少锁等待,开发阶段顺手开上,生产环境也建议保留。
3.3 数据访问层的封装思路
不要把 SQL 散落在控制器里。我在 services 目录下做一个集中封装,举个例子:
// src/services/userService.js const db = require('../models'); function findByUsername(username) { return db.prepare('SELECT * FROM users WHERE username = ?').get(username); } function createUser(username, passwordHash) { const result = db.prepare( 'INSERT INTO users (username, password_hash) VALUES (?, ?)' ).run(username, passwordHash); return result.lastInsertRowid; } module.exports = { findByUsername, createUser };封装的作用不是“多写一层”,而是让业务逻辑只关心“我要找这个用户”,不必关心“怎么找”。如果以后换数据库,只需要改 service 层内部实现。新手写项目最怕的就是一把梭,整个项目里到处是 SQL,最后想加一个字段,得翻遍所有文件,那是真的痛苦。
4. 核心接口实现:鉴权、会话和消息记录的关键代码
环境通了、表也建好了,接下来是“小智服务端”的主菜:核心接口。这一节覆盖注册登录、JWT 鉴权、创建会话、发消息和拉历史记录。
4.1 用户注册与密码加密
注册接口的业务逻辑很简单:接收用户名和密码,校验用户名是否已存在,密码哈希处理后入库,返回创建成功的提示。
密码加密我推荐使用bcryptjs,这是一个纯 JS 实现,不依赖原生编译,安装省心。
npm install bcryptjs jsonwebtoken// src/controllers/authController.js const bcrypt = require('bcryptjs'); const userService = require('../services/userService'); const { generateToken } = require('../utils/jwt'); async function register(req, res, next) { try { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ code: 'PARAM_MISSING', message: '用户名和密码不能为空' }); } const exists = userService.findByUsername(username); if (exists) { return res.status(409).json({ code: 'USER_EXISTS', message: '用户名已存在' }); } const hash = await bcrypt.hash(password, 10); const userId = userService.createUser(username, hash); res.status(201).json({ data: { id: userId, username } }); } catch (err) { next(err); } }bcrypt 的第二个参数 10 是盐的轮数,值越大计算越慢、越安全,同时响应也越慢。对于常规业务系统,10 是一个性能和安全性比较平衡的选择。这个参数不要随便调成 4 或 5,那基本等于没有加密强度。
4.2 JWT 鉴权中间件:让受保护接口认出合法请求
用户登录成功后,服务端签发一个 JWT。客户端后续请求时在请求头里带上这个 token,服务端验证通过就放行。
// src/utils/jwt.js const jwt = require('jsonwebtoken'); const config = require('../config'); function generateToken(payload) { return jwt.sign(payload, config.jwtSecret, { expiresIn: config.jwtExpiresIn }); } function verifyToken(token) { return jwt.verify(token, config.jwtSecret); } module.exports = { generateToken, verifyToken };对应的鉴权中间件:
// src/middleware/auth.js const { verifyToken } = require('../utils/jwt'); function authRequired(req, res, next) { const header = req.headers.authorization || ''; const token = header.startsWith('Bearer ') ? header.slice(7) : null; if (!token) { return res.status(401).json({ code: 'NO_TOKEN', message: '未提供访问令牌' }); } try { const decoded = verifyToken(token); req.user = decoded; next(); } catch (err) { return res.status(401).json({ code: 'INVALID_TOKEN', message: '令牌无效或已过期' }); } } module.exports = { authRequired };JWT 是无状态的,意味着服务端不需要存 session。但它的代价是“一旦签发,在有效期内很难主动失效”,所以密钥的管理尤为重要,生产环境不要把 JWT_SECRET 写成固定字符串,建议用足够随机的内容,并且定期轮换。
4.3 会话和消息接口:联动数据库的完整业务闭环
会话接口处理的是“创建新会话”和“拉取某用户的会话列表”。创建会话的代码逻辑很简单:根据当前登录用户的 ID,插入一条 conversations 记录。
// src/services/conversationService.js const db = require('../models'); function create(userId, title = '新会话') { const result = db.prepare( 'INSERT INTO conversations (user_id, title) VALUES (?, ?)' ).run(userId, title); return db.prepare('SELECT * FROM conversations WHERE id = ?').get(result.lastInsertRowid); } function listByUser(userId) { return db.prepare( 'SELECT * FROM conversations WHERE user_id = ? ORDER BY created_at DESC' ).all(userId); }消息接口稍微复杂一点,因为发消息涉及两件事:把用户的消息记录入库,然后生成一条“助手回复”。这里我先把“助手回复”设计成一条固定文案的模拟消息,将来接真实智能处理能力时,只需要替换 services 里那个生成回复的方法,接口层完全不用动。
// src/services/messageService.js const db = require('../models'); function addMessage(conversationId, role, content) { const result = db.prepare( 'INSERT INTO messages (conversation_id, role, content) VALUES (?, ?, ?)' ).run(conversationId, role, content); return db.prepare('SELECT * FROM messages WHERE id = ?').get(result.lastInsertRowid); }路由层把它串起来,形成完整的 HTTP 接口:
// src/routes/chat.js const express = require('express'); const { authRequired } = require('../middleware/auth'); const conversationService = require('../services/conversationService'); const messageService = require('../services/messageService'); const router = express.Router(); router.use(authRequired); router.post('/conversations', (req, res, next) => { try { const conversation = conversationService.create(req.user.id, req.body.title); res.status(201).json({ data: conversation }); } catch (err) { next(err); } }); router.get('/conversations', (req, res, next) => { try { const list = conversationService.listByUser(req.user.id); res.json({ data: list }); } catch (err) { next(err); } }); router.post('/conversations/:id/messages', (req, res, next) => { try { const message = messageService.addMessage(req.params.id, 'user', req.body.content); const reply = messageService.addMessage(req.params.id, 'assistant', '收到,我已经记下了你的问题。'); res.status(201).json({ data: { userMessage: message, reply } }); } catch (err) { next(err); } }); module.exports = router;注意路由里的router.use(authRequired),它会自动保护这个文件下所有接口,不会漏掉。这也是分层设计的好处——加新接口时只要放到这个路由文件里,鉴权逻辑自动生效。
5. 日志、错误处理与安全加固:让服务端真正具备可观测性
很多本地跑得很顺利的项目,一上线就崩,原因往往不是业务逻辑写错了,而是出了问题时你什么都看不见。日志和错误处理是服务端全流程里必须尽早补上的能力。
5.1 统一错误处理中间件:避免把堆栈直接抛给用户
Express 默认的错误处理会把详细堆栈返回给浏览器,这在生产环境是安全隐患,也给用户带来极差的体验。统一错误处理中间件的做法是:
// src/middleware/errorHandler.js const errorHandler = (err, req, res, next) => { console.error(`[error] ${req.method} ${req.path}:`, err.message); res.status(err.status || 500).json({ code: err.code || 'INTERNAL_ERROR', message: err.status === 500 ? '服务器内部错误' : err.message }); }; module.exports = { errorHandler };控制器里抛出业务错误时,建议挂上明确的 status 和 code,比如用户不存在、权限不足这类预期中的错误,不要把它们和未知错误混在一起返回。
5.2 请求日志与限流:记录每一次访问、拦住恶意高频请求
上线后最常用的排错手段是翻日志。我用一个简单的请求日志中间件记录方法和路径、请求耗时、状态码:
// src/middleware/logger.js const logger = (req, res, next) => { const start = Date.now(); res.on('finish', () => { console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${Date.now() - start}ms`); }); next(); };限流是保护服务不被刷爆的第一道防线。我常用express-rate-limit,给登录接口设置更严格的策略,毕竟它是暴力破解密码的主要目标:
const rateLimit = require('express-rate-limit'); app.use('/api/auth', rateLimit({ windowMs: 15 * 60 * 1000, max: 100, message: { code: 'TOO_MANY_REQUESTS', message: '请求过于频繁,请稍后再试' } }));5.3 安全加固的几个基础操作
- 生产环境关闭 X-Powered-By 响应头,避免暴露技术栈;
- 使用 helmet 中间件,一键设置常见安全响应头;
- SQL 查询一律用预编译语句(上面代码里用的
prepare().get()就是参数化查询),不要拼接字符串; - 数据库文件路径不要放在公开静态目录下,否则可能被直接下载;
- 所有环境变量里的敏感信息不要打日志。
6. 自动化测试:给服务端接口装上安全带
没有测试的服务端上线就像走钢丝。我不要求每个项目都做到 100% 覆盖率,但核心接口至少要有自动化测试兜底,尤其是鉴权和数据操作这类容易出问题的环节。
6.1 测试环境搭建
安装测试工具链:
npm install --save-dev jest supertestpackage.json 里配置:
"scripts": { "test": "jest --runInBand" }--runInBand让测试用例串行执行,避免多个测试同时操作同一个 SQLite 文件产生锁冲突。
6.2 写一组核心接口测试
下面这个测试覆盖了注册、登录、创建会话、发消息的主链路:
// src/tests/api.test.js const request = require('supertest'); const app = require('../app'); describe('小智服务端核心接口', () => { test('注册 -> 登录 -> 创建会话 -> 发送消息', async () => { const username = `tester_${Date.now()}`; const password = 'pass123456'; const regRes = await request(app) .post('/api/auth/register') .send({ username, password }); expect(regRes.statusCode).toBe(201); const loginRes = await request(app) .post('/api/auth/login') .send({ username, password }); expect(loginRes.statusCode).toBe(200); const token = loginRes.body.data.token; const convRes = await request(app) .post('/api/chat/conversations') .set('Authorization', `Bearer ${token}`) .send({ title: '测试会话' }); expect(convRes.statusCode).toBe(201); const msgRes = await request(app) .post(`/api/chat/conversations/${convRes.body.data.id}/messages`) .set('Authorization', `Bearer ${token}`) .send({ content: '你好' }); expect(msgRes.statusCode).toBe(201); expect(msgRes.body.data.reply.content).toBeTruthy(); }); test('未登录请求受保护接口应返回 401', async () => { const res = await request(app).get('/api/chat/conversations'); expect(res.statusCode).toBe(401); }); });写完测试后跑一下npm test,绿了再继续做后面的部署。这套测试还有一个隐藏价值:以后改代码,跑一遍测试就知道有没有把旧功能改坏,省去手动回归的重复劳动。
6.3 测试驱动开发的一点心得
我不是 TDD 的狂热信徒,但实践下来有个感受:测试先行的方式特别适合接口设计阶段。先把期望的请求和响应写出来,再写实现,接口契约会更清晰,不会出现“前端需要一个字段、后端完事之后发现漏了”的尴尬。写完一个接口顺手补一条测试,成本很低,长期收益却很高。
7. 部署上线与日常运维:从本地开发到线上服务
本地能跑只是第一步,真正上线部署又是另一套流程。我当初第一次部署时踩了不少坑,下面把最顺的路径完整写出来。
7.1 用 PM2 托管服务进程
本地调试时挂在前台无所谓,线上服务必须要有守护进程,否则一关终端服务就没了。PM2 是我最常用的进程管理工具,安装和使用都很简单:
npm install -g pm2 pm2 start src/server.js --name xiaozhi-server --env production常用命令:
pm2 status # 查看进程状态 pm2 logs xiaozhi-server # 实时日志 pm2 restart xiaozhi-server pm2 stop xiaozhi-serverPM2 最实用的能力是“崩溃自动重启”,服务进程异常退出后会自动拉起,大大减少服务器宕机时间。它还内置内存监控,能看到进程占用率,提前发现内存泄漏的苗头。
7.2 用反向代理统一入口:以常见的 Nginx 为例
我的习惯是服务进程只监听内网端口,对外统一走 Nginx 反向代理。这样能统一处理 HTTPS 证书、静态资源、负载均衡,而且服务端不用直接把 Node.js 暴露到公网,多一层隔离。
一个最小配置示例:
server { listen 80; server_name your-domain.example; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }配置文件改完后重新加载:
nginx -t nginx -s reload有域名之后,建议直接申请 HTTPS 证书并配置 443 端口,现在免费的证书渠道很多,配置过程也没那么复杂,这能有效避免接口数据在链路中被明文截获。
7.3 数据备份与定时任务
SQLite 是一个文件,备份最简单直接的方式就是定时复制数据库文件。我用 crontab 设了一个凌晨 3 点的定时任务:
0 3 * * * cp /data/xiaozhi.db /backup/xiaozhi_$(date +%Y%m%d).db这里有两个注意点:第一,备份前先用 SQLite 的 backup API 或者VACUUM INTO来生成一致的副本,直接复制正在被写入的 db 文件有概率产出不一致的快照;第二,定期清理旧备份,不然磁盘会被几个月的备份拖垮,我通常只保留近 7 天的备份。
8. 常见问题排查实录与避坑指南
最后这部分,整理我在搭“小智服务端”过程中真实踩过、也帮别人排查过的高频问题,每一条都对应一次实际的调试经历。
8.1 高频问题速查表
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
启动时报Cannot find module | 依赖没装全或路径写错 | 先确认npm install执行过,再检查 require 路径是否大小写一致 |
| 访问接口返回 404 | 路由挂载路径和请求路径不匹配 | 确认是否在 app.js 里挂载了/api前缀,路由文件内部又写了完整路径 |
| JWT 鉴权一直 401 | 请求头格式不对 | Authorization 必须是Bearer <token>,中间有空格,不是只传 token |
SQLite 报database is locked | 多进程并发写入 | 打开 WAL 模式(代码里已设置),确保测试是串行执行,不要开多个进程向同一个 db 文件写 |
| 接口返回堆栈信息 | 错误处理中间件没生效 | 确认app.use(errorHandler)放在所有路由之后 |
| PM2 启动后访问不了 | 端口或防火墙问题 | 先本地 curl 确认服务是否活着,再检查安全组和 Nginx 转发配置 |
| 修改代码后线上不生效 | 忘了重启进程 | pm2 reload xiaozhi-server,如果改的是环境变量,最好 restart |
8.2 数据库锁问题:SQLite 并发写的处理心得
在我实际体验中,SQLite 的锁问题是最容易让新手吓一跳的。它的底层机制是“同一时间只允许一个写事务”,如果代码里出现多个连接同时写,就会报错。解决方向有三个:
- 打开 WAL 模式,读写并发性能提升明显;
- 用的是 better-sqlite3 时,尽量复用同一个连接实例,避免频繁开关;
- 如果确实有多进程写入需求,说明该考虑迁移到 MySQL 或 PostgreSQL 了。
8.3 不要忽略环境差异
本地和服务器环境不一致也是个高频出坑的原因。我的解决方法是:初次服务器部署时,逐步对照节点版本、依赖版本、环境变量,把部署文档记录下来,作为一个标准操作流程反复对照。Linux 服务器的权限、防火墙、用户环境跟开发机差别不小,准备一份 checklist 能帮你每次都稳稳落地。
走到这一步,一个“小智服务端”就完成了从设计、开发、测试到部署上线的完整闭环。你手上现在有一套能跑通、有日志、有鉴权、有备份、能自动重启的真实服务端系统,这比看多少教程都管用。后面再往上加功能,比如接入真实处理能力、增加用户头像、做数据统计报表,都可以在这个骨架之上平稳扩展。最后分享一个我个人的经验:碰到问题先看日志,日志里没有就去复现,复现不了就在问题里加日志,不要凭感觉改代码——大部分线上事故,都是“感觉这里有问题”然后乱改改出来的。