技术栈:Tauri + Rust + SQLite(SQLCipher) + 原生前端
目标用户:0-500 人的中小型离散制造厂
测试规模:5000 物料 / 300 产品 / 12 万 BOM 明细 / 10000 订单
结论:中小工厂 PMC 场景下,本地优先架构相比云端 ERP 有明显优势
一、背景:中小工厂的 PMC 到底需要什么?
给国内中小工厂(50-500 人)做生产管理,我遇到的最真实需求不是"功能要多强",而是:
-
老板要打开就能看,不能等 10 秒
-
车间主任要当场查,翻页不能卡
-
采购要随手记,断电断网不能丢数据
-
全厂电脑通常2-5 台,都是普通办公机
这类客户会拒绝绝大多数大厂 ERP------不是因为功能不够,而是:
-
云端方案网络延迟大,局域网内 3-5 秒的响应无法接受
-
太重,一套 ERP 部署下来动辄几十万,中小工厂不买单
-
断网就瘫痪,尤其制造业经常网络不稳定
基于这些真实约束,我们设计了轻排 PMClite ------一个本地优先的轻量级 PMC 系统。本文不讨论功能,只讨论技术架构、性能测试方法与实测结果。
技术关键词:Tauri、Rust、SQLite、SQLCipher、AES-GCM、Ed25519、局域网同步、增量同步、BOM 多级展开、MRP 运算。
二、技术选型:为什么不是 Electron,也不是 Web 版?
2.1 桌面 vs Web 的取舍
| 维度 | Electron | Tauri | Web 版 |
|---|---|---|---|
| 包体大小 | 80-150 MB | 3-10 MB | 0(服务器承载) |
| 内存占用 | 200-400 MB | 30-80 MB | 浏览器 |
| 首屏加载 | 慢(Node 启动) | 快(原生) | 依赖网络 |
| 离线能力 | 好 | 好 | 差 |
| 前后端通信 | IPC(JSON) | IPC(JSON/Binary) | HTTP |
对中小工厂办公电脑(通常是 4-8 代 i3/i5,8-16G 内存),Electron 的内存和包体积都太浪费。而 Web 版在断网时完全不可用,这是制造业的硬伤。
最终选择 Tauri 1.x:
-
前端:原生 HTML + JS(不依赖框架,减小包体)
-
后端:Rust(高性能、内存安全)
-
数据库:SQLite + SQLCipher(单文件加密、零配置)
2.2 数据库为什么选 SQLite 而不是 MySQL/PostgreSQL
PMC 场景的数据特点:
-
单机写多读多(一台主机服务 2-5 台电脑)
-
单文件可备份(工厂 IT 只需要复制一个文件就能备份)
-
不需要跨机器事务(增量同步由应用层负责)
-
需要落盘加密(老板不希望生产数据随便被拷走)
SQLite 完美适配前三项,第四项用 SQLCipher 解决。核心表结构约 20 张,最大的是 inventoryLogs(10 万+ 行)。
三、数据模型与核心算法
3.1 数据模型概览
系统有 20 张核心表,PM C相关的核心是:
text
materials(物料) ──┬──> bomItems(BOM 明细,含 subproduct 类型)
products(产品) ──┘ │
├──> productionPlans(生产计划)
├──> workOrders(工单)
│ └──> pickLists(领料单,含明细)
├──> salesOrders(销售订单)
└──> purchaseOrders(采购订单,含明细)
BOM 支持多级嵌套 (最多测试到 8 层),用 type 字段区分:
-
type = 'material'→ 引用materials -
type = 'subproduct'→ 递归引用products(半成品)
3.2 关键算法:BOM 成本计算与齐套校验
BOM 递归成本(用于销售订单毛利计算):
rust
// 伪代码
fn calc_bom_cost(product_id, multiplier, visited) -> f64 {
if visited.contains(product_id) { return 0 } // 防循环引用
visited.insert(product_id)
let items = _bom_by_product.get(product_id) // O(1) 索引
let mut total = 0.0
for item in items {
if item.type == 'material' {
total += item.quantity * material.unitPrice * multiplier
} else if item.type == 'subproduct' {
total += calc_bom_cost(item.subProductId, item.quantity * multiplier, visited)
}
}
visited.remove(product_id)
total
}
齐套校验(用于工单派工前的物料检查):
rust
// 伪代码
fn check_kit(work_order) -> KitResult {
// 递归展开 BOM,检查每层库存是否充足
// 半成品优先用库存,不足时展开下级
// 返回 { allAvailable, shortages[] }
}
性能关键点 :这两个算法都会被大量重复调用 ,如果每次都在全表里 filter,复杂度是 O(N×M)。我们通过预建索引 (Map<productId, BomItem[]>)把查询降到 O(1),配合结果缓存 (按 db._version 版本管理),重复调用几乎零成本。
3.3 MRP 运算原理
MRP(Material Requirements Planning)核心公式:
text
建议采购量 = 总需求量 - 当前库存 - 在途采购量 + 安全库存
其中:
-
总需求量 = Σ(生产计划数量 × 多级 BOM 用量)
-
在途采购量 = Σ(采购订单未入库数量)
-
安全库存 = 总需求 × 安全库存比例
运算复杂度主要在多级 BOM 展开 ------5000 物料、300 产品、12 万 BOM 行,如果每次递归都扫全表,单次 MRP 需要几千万次比较。实测优化后(用索引 + 缓存)2.3 秒完成全量运算。
四、测试数据是怎么生成的?(可复现方法)
性能测试最怕"数据造假"------很多厂商的测试报告里,数据量看着大,但结构不真实。我们做了一个开源的数据生成器(纯 HTML + JS,单文件),让任何人都能生成自己的测试数据。
4.1 数据结构模拟
物料方面,我们按电子制造行业真实结构生成:
text
物料编码:类别前缀 + 6位序号(如 RES-00001、CAP-00342)
物料名称:类别 + 型号 + 序号(如 电阻0805型-001)
规格:电压 + 电流 + 精度(如 10V 5A 50%)
分类覆盖 20 种常见电子元件(电阻/电容/电感/二极管/MCU/PCB/连接器/结构件等),供应商池 18 家(深圳华强、深南电路、意法半导体等)。
为什么用行业真实分类,而不是 材料001、材料002? 因为业务代码里经常做 materials.filter(m => m.category === '电阻') 这类操作,如果所有物料的 category 都是一样的字符串,测试出来的性能是失真的。
4.2 BOM 层级模拟
生成器用概率模型控制 BOM 层级:
javascript
for (let i = 0; i < itemsPerProduct; i++) {
// 30% 概率插入半成品(若层级深度允许)
const canUseSub = product.type !== 'semi'
&& semiProducts.length > 0
&& random() < 0.3;
if (canUseSub) {
// 从半成品池里随机选一个,避免自引用和重复
bomItems.push({ type: 'subproduct', subProductId, quantity: randInt(1, 5) });
} else {
bomItems.push({ type: 'material', materialId, quantity: randInt(1, 20) });
}
}
这个策略生成出的 BOM 结构特点:
-
大产品(成品)→ 中等层数
-
半成品也各自有 BOM → 递归深度可以达到 4-6 层
-
无循环引用(用
usedRefsSet 保证)
4.3 时间轴的"真实感"处理(最关键)
很多测试数据生成的订单都是随机日期 ,导致订单的 orderDate 可能晚于 deliveryDate,"已完成"订单的交货日在未来------一导入系统就露馅。
我们的生成器用了时间联动策略:
javascript
const NOW = new Date();
const DAY = 86400000;
if (status === '已完成') {
// 3-12 个月前下单,交付日 = 下单 + 15~45 天(但必须在过去)
orderDate = daysAgo(randInt(90, 365));
deliveryDate = min(addDays(orderDate, randInt(15, 45)), NOW - 10*DAY);
invoiceDate = addDays(orderDate, randInt(5, 20));
paymentDate = min(addDays(deliveryDate, randInt(10, 60)), NOW);
} else if (status === '待发货') {
// 细分为"逾期 / 即将到期 / 正常"三种场景,用于测试预警功能
const variant = random();
if (variant < 0.15) {
// 已逾期 15%:交期已过 1-30 天
deliveryDate = daysAgo(randInt(1, 30));
} else if (variant < 0.35) {
// 即将到期 20%:0-3 天内
deliveryDate = daysFromNow(randInt(0, 3));
} else {
// 正常 65%:7-45 天后
deliveryDate = daysFromNow(randInt(7, 45));
}
}
这样生成的数据有 3 个好处:
-
业务逻辑自洽------所有状态与时间的组合都符合现实
-
预警功能可测------逾期/即将到期/缺料预警都能被触发
-
统计口径可验证------KPI 数字能反映真实业务分布
4.4 库存日志的"加权分布"
真实的库存日志不是均匀分布------近期日志多,远期日志稀疏。我们的生成器用了三段加权:
javascript
function weightedDaysAgo() {
const r = random();
if (r < 0.4) return randInt(1, 30); // 40% 在近 30 天
if (r < 0.7) return randInt(31, 90); // 30% 在 30-90 天
return randInt(91, 365); // 30% 在 90-365 天
}
这样能准确测试**"近 30 天出库量"这类统计逻辑的性能**,而不是拿均匀分布的数据自欺欺人。
4.5 生成器的可复现性
生成器用确定性伪随机(LCG 算法),同一组参数 + 同一种子(默认取当前时间戳)每次结果不同,但能通过参数完全复现数据规模:
| 预设 | 物料 | 产品 | 订单 | BOM 行数 | 数据体积 |
|---|---|---|---|---|---|
| 演示级 | 10 | 5 | 5 | 25 | < 10 KB |
| 小型厂 | 500 | 50 | 300 | 2500 | ~ 500 KB |
| 中型厂 | 5000 | 300 | 10000 | 120000 | ~ 12 MB |
| 大型厂 | 30000 | 2000 | 20000 | 800000 | ~ 78 MB |
| 极限 | 100000 | 5000 | 50000 | 4000000 | ~ 400 MB |
任何人都能用这个生成器造自己的数据集,1 分钟出结果。这是保证测试可信度的前提。

五、性能测试方案与结果
5.1 测试环境
不做"跑分机器"测试,用主流办公配置:
| 项目 | 配置 |
|---|---|
| CPU | Intel i5-10400(6C12T,2019 年发布) |
| 内存 | 16 GB DDR4-2666 |
| 硬盘 | 512 GB SATA SSD(非 NVMe) |
| 系统 | Windows 10 专业版 22H2 |
| 软件版本 | 轻排 PMClite v1.x |
选这个配置的原因:这是 2020-2023 年国内中小工厂办公电脑最常见配置,测出来的数据才有意义。
5.2 测试方法
-
每个操作连续测试 10 次,去掉第一次(含 JIT 冷启动、系统缓存未命中等因素)
-
取后 9 次的平均值
-
记录最差的一次作为稳定性指标
-
数据库是 SQLCipher 加密(不是明文 SQLite),这会更慢
5.3 测试结果(5000 物料 / 10000 订单 / 12 万 BOM)
启动与首屏
| 操作 | 平均耗时 | 最差耗时 |
|---|---|---|
| 双击 → 登录框出现 | 2.1 秒 | 3.2 秒 |
| 登录 → 仪表盘首屏可见 | 1.8 秒 | 2.6 秒 |
| 仪表盘 KPI 数值完全加载 | 1.4 秒 | 2.1 秒 |
首次启动因为要解密 SQLCipher + 加载全量数据,会慢一些。第二次启动得益于 OS 文件缓存,通常可以做到 1.2-1.5 秒。
列表页性能
| 操作 | 平均耗时 |
|---|---|
| 物料列表(5000 行)首屏 | 0.30 秒 |
| 物料列表翻页(每页 100 条) | 0.18 秒 |
| 物料搜索"电阻"(模糊匹配) | 0.22 秒 |
| 销售订单列表(10000 单) | 0.50 秒 |
| 销售订单翻页 | 0.30 秒 |
| 采购订单列表(3000 单) | 0.35 秒 |
| 生产工单列表(4000 单) | 0.60 秒 |
| 库存页面(5000 行) | 0.25 秒 |
| 财务页面 | 0.40 秒 |
BOM 与计算类
| 操作 | 平均耗时 | 说明 |
|---|---|---|
| BOM 树首层展开(4 层结构) | 0.50 秒 | 默认只展开第一层 |
| 展开 100 项半成品 | 0.15 秒 | 懒加载 |
| 单产品 BOM 成本计算(40 项) | 0.08 秒 | 缓存命中后 <1ms |
| MRP 全量运算(300 产品) | 2.30 秒 | 涉及 12000 BOM 行展开 |
| 工单齐套校验(4000 单,首次) | 3.50 秒 | BOM 递归 + 库存校验 |
| 工单齐套校验(缓存命中) | 0.05 秒 | 按版本号失效 |
局域网同步(5 台电脑协作)
| 场景 | 耗时 |
|---|---|
| 主机:客户端推送一条订单变更 | 0.30 秒 |
| 客户端:拉取全量快照(首次连接) | 2.80 秒 |
| 5 台电脑同时操作,平均响应 | 0.50 秒 |
5.4 极限规模对比(30000 物料)
| 指标 | 5000 物料 | 30000 物料 | 倍数 |
|---|---|---|---|
| 启动到登录框 | 2.1 秒 | 5.8 秒 | 2.8x |
| 物料列表打开 | 0.30 秒 | 1.20 秒 | 4x |
| 销售订单翻页 | 0.30 秒 | 0.60 秒 | 2x |
| 仪表盘 KPI 完整 | 1.4 秒 | 4.5 秒 | 3.2x |
| MRP 运算 | 2.30 秒 | 8.50 秒 | 3.7x |
| BOM 树打开 | 0.50 秒 | 1.80 秒 | 3.6x |
结论 :30000 物料时依然可用 ,但启动和仪表盘会明显变慢(3-5 秒)。这已经是 Tauri + SQLite + 加密数据库这类架构在本地机器上的瓶颈区。
六、性能优化的几个关键实践
6.1 前端:索引化 + 分页 + 懒渲染
最初实现的问题 :产品页、BOM 页使用 .filter() 在内存里全量遍历,5000 物料 + 12000 BOM,每次切换页面都要遍历几十万次。
优化方案:
-
索引化 :应用启动时构建
_materialIdx、_productIdx、_bomByProduct、_bomRefsBySub、_inventoryByMat等 Map 索引,后续查询全部 O(1) -
分页 :所有列表页每页 100 条,分页状态存
_pageState -
懒渲染:BOM 树默认只展开 level 0,点击才递归渲染子树
收益 :产品页切换时间从 3-5 秒降到 0.3 秒。
6.2 后端:N+1 查询修复
最初实现的问题:加载 10000 个采购订单时,代码是这样的:
rust
// ❌ N+1 查询,10000 订单 = 10000 次 SQL
for po in purchase_orders {
let items = query("SELECT * FROM purchaseOrderItems WHERE poId = ?", po.id);
}
优化方案:一次查全表,内存分组:
rust
// ✅ 2 次查询
let all_items = read_table(conn, "purchaseOrderItems")?; // 全表
let mut by_po_id: HashMap<String, Vec<Value>> = HashMap::new();
for item in all_items {
by_po_id.entry(item.poId).or_default().push(item);
}
for po in purchase_orders {
po.items = by_po_id.remove(&po.id).unwrap_or_default();
}
收益 :数据库加载阶段从 4-5 秒 降到 1-2 秒。
6.3 缓存:版本号管理 + 空跑优化
最初实现的问题 :computeChanges() 每次保存都做全表深拷贝对比,20MB 数据深拷贝耗时 200-500ms。
优化方案:
javascript
function computeChanges() {
// 1. 先做整表哈希快速判断(行数 + 内容哈希)
const currentHash = quickHash(db[table]);
const oldHash = quickHash(_dbSnapshot[table]);
if (currentHash === oldHash) continue; // 无变化,跳过
// 2. 有变化才逐行对比
// 3. 单行对比也用哈希,比 JSON.stringify 快 3-5 倍
}
收益 :保存操作从 200-500ms 降到 5-10ms(无变化时)。
6.4 Dashboard:从"全量遍历"到"服务端聚合"
最初实现的问题:仪表盘 KPI 需要遍历 20000 订单 + 15000 工单 + 100000 日志,耗时 4-5 秒。
优化方案:加 SQL 聚合命令:
sql
-- 一次查询返回 6 个 KPI
SELECT
(SELECT COUNT(*) FROM workOrders WHERE status = '进行中') AS inProgressWO,
(SELECT COUNT(*) FROM salesOrders WHERE status = '待发货' AND deliveryDate < date('now')) AS overdueSales,
(SELECT SUM(i.quantity * m.unitPrice) FROM inventory i LEFT JOIN materials m ON ...) AS inventoryValue,
...
收益 :仪表盘 KPI 从 4-5 秒降到 50-100ms。
6.5 局域网同步:增量推送 + 全量拉取的混合策略
同步机制:
-
写方向 :客户端修改 →
computeChanges()计算差异(几十个 change)→ POST/api/push到主机 -
读方向 :主机版本号变化 → 客户端 GET
/api/snapshot拉全量
已知瓶颈 :客户端拉取是全量快照,44MB 数据在千兆网下 2-3 秒,百兆网 20-30 秒。5000 物料场景下 2-3 秒可以接受,30000 物料场景下会明显变慢。
未来优化方向 :把"全量拉取"改成"基于版本的增量拉取"------主机维护一个 change_log 表,客户端按 last_version 拉取增量。
七、这套架构适合多大规模的工厂?
基于上述测试,我们把适用规模明确分层:
7.1 适配矩阵
| 工厂规模 | 员工数 | 物料 | 年订单 | 协作电脑 | 适配度 |
|---|---|---|---|---|---|
| 微型厂 | 5-20 人 | <3000 | <3000 | 1-2 台 | 🟢 完美适配 |
| 小型厂 | 20-80 人 | 3000-10000 | 3000-10000 | 2-3 台 | 🟢 推荐 |
| 中型厂 | 80-200 人 | 10000-30000 | 10000-30000 | 2-5 台 | 🟡 可用(本文区间) |
| 中大型厂 | 200-500 人 | 30000-80000 | 30000-80000 | 5-10 台 | 🟡 可用,需配合服务端分页 |
| 大型厂 | 500+ 人 | >80000 | >80000 | >10 台 | 🔴 不推荐 |
7.2 明确不适用场景
-
跨厂区 / 跨网络协作:同步依赖 UDP 广播,走不出路由器
-
手机 / 平板访问:当前仅支持桌面
-
外网访问:不做云端,纯本地优先
-
关键业务唯一系统:更适合作为 PMC 辅助工具,不是全功能 ERP
7.3 行业适配
适配度最高的行业:
-
电子制造(SMT / PCBA / 组装)
-
机械加工(CNC / 钣金 / 焊接)
-
五金 / 塑胶制品
-
食品 / 化妆品贴牌
不适合的行业:
-
流程制造(化工、冶炼、制药,工艺路线复杂且连续)
-
项目型制造(大型设备、模具、工程安装,周期长)
八、为什么本地优先架构能赢在这个规模区间?
把轻排 PMClite 的性能和常见云 ERP 做对比(同类数据量、同类业务场景):
| 维度 | 云 ERP | 本地优先 PMC |
|---|---|---|
| 局域网内响应 | 1-3 秒(网络延迟) | 0.3-0.6 秒(本地 IO) |
| 断网可用 | ❌ 不可用 | ✅ 完全可用 |
| 数据安全 | 依赖厂商 | ✅ SQLCipher 加密本地存储 |
| 备份恢复 | 需要厂商支持 | ✅ 复制一个 .db 文件 |
| 部署成本 | 万元级 | ✅ 一次性授权 |
| 跨厂区 | ✅ 支持 | ❌ 不支持 |
结论 :对于 0-500 人、单一厂区、无跨网络需求的中小工厂,本地优先架构在性能、成本、安全、可用性四个维度都明显优于云 ERP。这不是"技术更先进",而是"架构匹配场景"。
九、测试数据与方法的公开
为了保证本文所有数据可复现,我们公开了:
-
数据生成器:单文件 HTML,浏览器打开即用,支持自定义所有参数(物料数、订单数、BOM 层级、时间跨度等)
-
测试数据集:5000 物料 / 30000 物料 / 100000 物料,可直接导入复现测试
-
测试方法说明:与本文测试方案一致
如果你工厂的数据量在本文测试区间,可以下载生成器造一份相似数据,自己跑一遍------这才是最可信的验证方式。
十、总结
这篇文章没有宣传功能,只讲了架构、算法、测试方法、优化实践。核心结论:
-
本地优先架构 + SQLite 加密存储,是 0-500 人中小工厂 PMC 场景的高性价比选择
-
5000 物料 / 10000 订单 规模下,轻排 PMClite 的启动、翻页、搜索、计算全部在亚秒级
-
30000 物料依然可用,但启动和仪表盘会明显变慢(3-5 秒),需要后续分段加载优化
-
局域网同步适合 2-5 台电脑协作,超过 10 台需要考虑服务端方案
-
所有测试数据可通过公开的数据生成器复现
技术选型没有银弹------大厂 ERP 有它的场景,本地优先也有它的场景。关键是承认边界、公开数据、让用户自己验证。
附录:技术栈清单
| 层次 | 技术 |
|---|---|
| 桌面容器 | Tauri 1.x |
| 后端语言 | Rust |
| 前端 | 原生 HTML5 + Vanilla JS |
| 数据库 | SQLite + SQLCipher |
| 加密算法 | AES-GCM(数据) / Ed25519(授权) / Argon2id(密钥派生) |
| 局域网发现 | UDP 广播(31337 端口) |
| 局域网同步 | tiny_http + JSON over HTTP |
| 图表 | Chart.js |
| Excel 导入导出 | SheetJS |
| 二维码 | qrcode.js |
关键词:生产管理、PMC 系统、轻排 PMClite、Tauri、SQLite、SQLCipher、局域网同步、BOM 多级展开、MRP 运算、本地优先架构、中小工厂数字化、5000 物料性能测试。