DeWatch是一个用Node.js驱动的虚拟钟表工厂项目,它把传统制表车间搬进了代码世界:一块手表从齿轮、发条、表壳等零件入库开始,要依次经过机芯装配、表盘组装、走时校准、防水测试等工序才能出厂。每道工序耗时不同、可能失败、失败后还要返修,这天然就是一条异步流水线,而异步恰恰是Node.js最擅长的事情。本文把这套系统的架构设计、数据建模、流水线调度、实时监控和质检溯源逐一拆开讲透,给出的代码都可以直接跑起来。

一、钟表流水线为什么天然适合Node.js
先说结论:制造流水线和Node.js的事件循环,本质上是同一种东西。车间里每台设备做完一道工序就把半成品传给下一台,自己立刻腾出手接新活;Node.js里每个异步任务完成后就把结果交给回调,线程立刻去处理别的请求。两者都不需要谁停下来干等。用Node.js建模装配线,几乎不需要额外的抽象转换,代码结构可以直接照着车间的物理布局来写。
再对比一下别的方案。用Java做这类系统,通常要起一个线程池,每个工序一个工作线程,线程之间靠阻塞队列传递半成品,写起来要处理锁、要担心死锁,调试成本不低。用Python配合Celery也能做,但引入了消息代理,部署重量立刻上去了。而Node.js单线程跑事件循环,所有工序状态都在同一个进程里,没有共享变量的竞争问题,EventEmitter一行代码就能完成工序间的交接,对中小规模的模拟工厂来说是最轻量的选择。
还有一个容易被忽略的好处:Node.js的定时器和异步IO模型,让模拟每道工序的耗时变得非常自然。用setTimeout模拟装配耗时,用随机数模拟良品率,整条线的节奏、瓶颈、返修风暴都能真实地跑出来,非常适合用来做生产调度算法的沙盘推演。
二、整体架构与SQLite数据建模
整个系统分成四层:最底下是数据层,用SQLite存零件库存、手表档案和工序日志;中间是调度层,也就是装配流水线本体,负责把一块表从第一道工序推到最后一道;上面是监控层,通过WebSocket把每个工位的动态推给浏览器看板;最外围是接口层,用Express暴露几个REST接口,供外部下达生产订单。层与层之间只靠事件和数据库交互,任何一层出问题都不影响其他层继续运转。
数据库选SQLite而不是MySQL或MongoDB,理由很实际:better-sqlite3这个库是同步API,调用即返回,不用写一堆await,事务支持也完整;单文件数据库拷走就能备份,对一条虚拟装配线来说绰绰有余。表结构只需要三张:parts表管零件库存,watches表记录每块表的当前状态,stage_logs表是核心,每过一道工序就插一条记录,将来追溯全靠它。
const Database = require('better-sqlite3');
const db = new Database('dewatch.db');
db.exec(`
CREATE TABLE IF NOT EXISTS parts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
category TEXT NOT NULL,
stock INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS watches (
serial TEXT PRIMARY KEY,
model TEXT NOT NULL,
status TEXT DEFAULT 'pending',
current_stage TEXT DEFAULT '零件入库',
created_at TEXT DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS stage_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
serial TEXT NOT NULL,
stage TEXT NOT NULL,
result TEXT NOT NULL,
note TEXT DEFAULT '',
ts TEXT DEFAULT (datetime('now', 'localtime'))
);
`);建表语句里有几个细节值得说。watches表的serial字段直接做主键,因为序列号天然唯一,省掉自增id再查一次的麻烦;stage_logs表不设外键约束,只建普通索引,写入失败不会阻塞流水线,宁可日志缺一条也不能让产线停机,这是制造系统的取舍原则;created_at用SQLite自带的datetime函数生成,省得应用层再算一遍时间。
三、用EventEmitter把工序串成流水线
流水线的核心思路是:把每道工序看成一个事件名,工序完成时emit下一道工序的事件,一块表就在事件之间被接力传递。这样做最大的好处是解耦,新增一道工序只需要在数组里插一项,删掉一道工序改一下数组,流水线类本身一行都不用动,完全符合开闭原则。先把工序定义成一份配置数组,再写流水线类。
const EventEmitter = require('events');
const STAGES = [
{ name: '零件入库', cost: 200, failRate: 0.02 },
{ name: '机芯装配', cost: 800, failRate: 0.08 },
{ name: '表盘组装', cost: 600, failRate: 0.05 },
{ name: '走时校准', cost: 500, failRate: 0.06 },
{ name: '防水测试', cost: 300, failRate: 0.03 }
];
class AssemblyLine extends EventEmitter {
constructor(stages) {
super();
this.stages = stages;
stages.forEach(stage => {
this.on(stage.name, watch => this.run(stage, watch));
});
}
async run(stage, watch) {
console.log(`表款 ${watch.serial} 进入工序:${stage.name}`);
await new Promise(resolve => setTimeout(resolve, stage.cost));
const pass = Math.random() > stage.failRate;
if (!pass) {
this.emit('defect', { watch, stage: stage.name });
return;
}
const idx = this.stages.indexOf(stage);
const next = this.stages[idx + 1];
if (next) {
this.emit(next.name, watch);
} else {
this.emit('finished', watch);
}
}
start(watch) {
this.emit(this.stages[0].name, watch);
}
}这段代码里有三个关键点。第一,run方法里的await只阻塞当前这块表的流转,不会阻塞整个进程,事件循环照常处理其他表,这就是单线程也能并发跑多块表的原因。第二,失败不抛异常而是emit一个defect事件,把异常处理从主流程里剥离出去,返修逻辑、告警逻辑都挂在defect上,互不干扰。第三,start方法只负责点火,从第一道工序开始emit,后面全靠事件自己接力,调度层没有任何while循环去轮询状态。执行new AssemblyLine(STAGES)即可得到流水线实例line。
每道工序跑完还要落库,这一步同样用事件挂载,不侵入流水线主类。
function recordStage(serial, stage, result, note) {
db.prepare(
'INSERT INTO stage_logs (serial, stage, result, note) VALUES (?, ?, ?, ?)'
).run(serial, stage, result, note);
db.prepare(
'UPDATE watches SET current_stage = ?, status = ? WHERE serial = ?'
).run(stage, result, serial);
}
line.on('defect', ({ watch, stage }) => {
recordStage(watch.serial, stage, 'defect', '工序不合格,转入返修');
});
line.on('finished', watch => {
recordStage(watch.serial, '成品出厂', 'passed', '全部工序通过');
});recordStage一次写两张表,借助better-sqlite3的同步特性保证写完再返回,不存在读到半写状态的问题。如果将来换成MySQL,把这两次写包进一个事务即可,业务逻辑完全不用变。
四、WebSocket实时监控看板
车间主任最关心的不是数据库里的最终结果,而是此刻每条线上发生了什么。传统做法是前端每隔几秒请求一次接口轮询,既浪费带宽,实时性又差。WebSocket长连接是更合适的方案:服务器有事件就推,没事件就安静待着,一块表从入库到出厂的每一次状态跳变,看板上都能在毫秒级看到。
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ port: 8081 });
function broadcast(payload) {
const msg = JSON.stringify(payload);
wss.clients.forEach(client => {
if (client.readyState === 1) {
client.send(msg);
}
});
}
line.on('finished', watch => {
broadcast({ type: 'finished', serial: watch.serial, model: watch.model });
});
line.on('defect', info => {
broadcast({ type: 'defect', serial: info.watch.serial, stage: info.stage });
});broadcast函数遍历所有在线客户端推送消息,readyState等于1表示连接已建立,跳过还在握手阶段的连接可以避免报错。注意这里刻意没有做消息队列和离线补发,因为看板只关心实时动态,历史数据查数据库更准确,两种职责分开反而清晰。
前端页面只需要十几行代码就能接上这条数据流。
<div id="board"></div>
<script>
const ws = new WebSocket('ws://127.0.0.1:8081');
ws.onmessage = e => {
const data = JSON.parse(e.data);
const board = document.getElementById('board');
const row = document.createElement('p');
row.textContent = data.serial + ' 状态:' + data.type;
board.appendChild(row);
};
</script>看板页面上放一个<div>容器,收到消息就追加一行文字,用textContent而不是innerHTML拼内容,避免序列号里混进奇怪字符破坏页面结构。实际项目里可以把文字换成状态卡片,再配合CSS过渡做出工序流转的动画效果,观感会好很多。
五、质检与序列号溯源
最后一块拼图是质检和溯源。序列号用crypto模块生成,包含型号、时间戳和随机段,全局唯一且肉眼可读,比纯UUID对车间更友好。质检函数模拟四项检查,任何一项不过就整表拒收,检查报告完整保留,方便后续分析哪一项是短板。
const crypto = require('crypto');
function makeSerial(model) {
const stamp = Date.now().toString(36).toUpperCase();
const rand = crypto.randomBytes(2).toString('hex').toUpperCase();
return `DW-${model}-${stamp}-${rand}`;
}
function inspect(watch) {
const checks = ['走时精度', '防水深度', '外观检查', '指针校准'];
const report = checks.map(item => {
return { item, ok: Math.random() > 0.05 };
});
const failed = report.filter(r => !r.ok);
return failed.length === 0
? { verdict: 'passed', report }
: { verdict: 'rejected', report, failed };
}
function trace(serial) {
return db.prepare(
'SELECT stage, result, note, ts FROM stage_logs WHERE serial = ? ORDER BY ts'
).all(serial);
}trace函数是整个系统的价值收口:拿一个序列号进去,返回这块表经过的每一道工序、每一项结果和精确到秒的时间戳。真实工厂里这叫单件流追溯,出了客诉能定位到具体工序和时间段;在我们这个模拟系统里,它同样能回答哪个工序返修率最高、哪段时间产出最低这类分析问题,把stage_logs表积累的数据真正用起来。
至此DeWatch的骨架就完整了:EventEmitter负责流转,SQLite负责记忆,WebSocket负责直播,crypto负责身份。整套代码不到三百行,却覆盖了一条制造线最核心的调度、监控和追溯能力。想继续深入,可以给defect事件加上返修重试队列,给parts表加上库存扣减和补货预警,或者用cluster模块把多条装配线跑满CPU核数,每一步都是一次不错的练习。