0-500 人工厂的生产 PMC 系统怎么做?基于 Tauri + SQLite 的性能架构实践与 5000 物料压测实录

技术栈:Tauri + Rust + SQLite(SQLCipher) + 原生前端

目标用户:0-500 人的中小型离散制造厂

测试规模:5000 物料 / 300 产品 / 12 万 BOM 明细 / 10000 订单

结论:中小工厂 PMC 场景下,本地优先架构相比云端 ERP 有明显优势


一、背景:中小工厂的 PMC 到底需要什么?

给国内中小工厂(50-500 人)做生产管理,我遇到的最真实需求不是"功能要多强",而是:

  • 老板要打开就能看,不能等 10 秒

  • 车间主任要当场查,翻页不能卡

  • 采购要随手记,断电断网不能丢数据

  • 全厂电脑通常2-5 台,都是普通办公机

这类客户会拒绝绝大多数大厂 ERP------不是因为功能不够,而是:

  1. 云端方案网络延迟大,局域网内 3-5 秒的响应无法接受

  2. 太重,一套 ERP 部署下来动辄几十万,中小工厂不买单

  3. 断网就瘫痪,尤其制造业经常网络不稳定

基于这些真实约束,我们设计了轻排 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 层

  • 无循环引用(用 usedRefs Set 保证)

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 个好处:

  1. 业务逻辑自洽------所有状态与时间的组合都符合现实

  2. 预警功能可测------逾期/即将到期/缺料预警都能被触发

  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,每次切换页面都要遍历几十万次。

优化方案:

  1. 索引化 :应用启动时构建 _materialIdx、_productIdx、_bomByProduct、_bomRefsBySub、_inventoryByMat 等 Map 索引,后续查询全部 O(1)

  2. 分页 :所有列表页每页 100 条,分页状态存 _pageState

  3. 懒渲染: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。这不是"技术更先进",而是"架构匹配场景"。


九、测试数据与方法的公开

为了保证本文所有数据可复现,我们公开了:

  1. 数据生成器:单文件 HTML,浏览器打开即用,支持自定义所有参数(物料数、订单数、BOM 层级、时间跨度等)

  2. 测试数据集:5000 物料 / 30000 物料 / 100000 物料,可直接导入复现测试

  3. 测试方法说明:与本文测试方案一致

如果你工厂的数据量在本文测试区间,可以下载生成器造一份相似数据,自己跑一遍------这才是最可信的验证方式。


十、总结

这篇文章没有宣传功能,只讲了架构、算法、测试方法、优化实践。核心结论:

  1. 本地优先架构 + SQLite 加密存储,是 0-500 人中小工厂 PMC 场景的高性价比选择

  2. 5000 物料 / 10000 订单 规模下,轻排 PMClite 的启动、翻页、搜索、计算全部在亚秒级

  3. 30000 物料依然可用,但启动和仪表盘会明显变慢(3-5 秒),需要后续分段加载优化

  4. 局域网同步适合 2-5 台电脑协作,超过 10 台需要考虑服务端方案

  5. 所有测试数据可通过公开的数据生成器复现

技术选型没有银弹------大厂 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 物料性能测试。

相关推荐
l1t1 小时前
DeepSeek总结的 DuckDB 2.0 文档新内容
服务器·网络·数据库
软件工程师文艺1 小时前
制造业数字化转型路线图:从信息化总体规划到智能工厂的五阶段演进
架构
各类产品分享11 小时前
工业现场 AR 巡检系统的技术架构与落地难点解析
架构·ar·ar巡检
GOWIN革文品牌咨询1 小时前
长周期B2B线索培育的系统设计:状态字段、触发器与转交条件
大数据·架构
IvorySQL2 小时前
PostgreSQL 日报| PG19 主备切换挂起修复(10 月 17 日)
数据库·postgresql
roman_日积跬步-终至千里2 小时前
【数据Agent论文·查询与语义|11】EntSQL:给了业务文档,模型为何还是写不对SQL?
数据库·人工智能·sql·数据分析
2601_962885722 小时前
如何用 Python 算最大回撤和回撤恢复期?
java·数据库·python
zcn1262 小时前
Left join+or exists+like关联优化
数据库·sql优化改写
我科绝伦(Huanhuan Zhou)2 小时前
Oracle 表空间碎片整理实战:shrink space 与 move table 全解析
数据库·oracle