ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

单机版BOM管理系统:物料主数据、展开反查与成本滚算

单机版BOM管理系统:物料主数据、展开反查与成本滚算

简介:这份资源提供捷远BOM管理系统的单机版程序,免费且免安装,主要面向中小型生产装配企业的物料管理、成本核算人员,以及想了解BOM搭建流程的初学者,用于按订单自动计算物料需求数量、理清产品层级结构。压缩包共12个文件、约3.05MB,其中exe为主程序,mdb为内置数据库,8个apt为领料、预算等报表模板,另配一份xls实例和一个htm说明文档,打开即可使用,无需配置环境。目前已有1583人学习下载。资源以台式电脑组装这一三层结构为例,配套说明与实例表格展示了从建立BOM、维护零件信息到领料和预算的完整流程,读者可对照搭建自己的产品结构,理解多层级物料拆解与需求汇总思路;报表模板也能直接参照,用于生产性企业的日常单据输出。

1. 从一张四百行的 Excel 说起:单机版 BOM 到底解决什么问题

有人把一份 Excel 甩过来,三列:成品、子件、用量,往下拉了四百多行,问这台机器换两个件成本差多少。没有结构化的物料清单,这问题只能靠人一行行扒,扒完还不敢保证没漏层。捷远BOM管理系统 单机版的定位就卡在这个痛点上:把一个成品拆成树,物料主数据、BOM 头行、版本生效日期都落到库里,展开、反查、成本滚算一次算清。它是绿色免安装的单机程序,解压即用,数据就一个本地库文件;适合几十人到一两百人规模、多品种小批量的装配加工场景,适合没有 IT 部门、也不想为了一套 BOM 上云上系统的厂子。它不是 ERP,但能把「这台机器由什么组成、改一个件影响哪些成品、成本大概多少」这三件事答清楚。

2. 功能骨架拆解:物料主数据、BOM 头行与版本三张底牌

把这套系统当成一个黑匣子会很吃亏,因为它所有能力都长在三张表上:物料主数据、BOM 头、BOM 行。搞明白这三张表怎么分工,后面无论是查数据、改字段,还是自己加报表,都不会乱。

2.1 物料主数据:所有查询的入口

物料主数据是整套系统的地基。BOM 行里只存物料编码,不存名称和规格,展开时才去主数据里取。这种「只存编码」的做法是血泪经验:如果 BOM 行里冗余存了名称,某天工程改了物料描述,历史 BOM 里的旧名字就和新名字对不上,对账时全是麻烦。

主数据里至少要有这几类字段:编码、名称、规格、单位、物料类型(采购件、自制件、虚拟件)、标准成本、低层码、启用状态。

-- 物料主数据:BOM 行只引用 ItemCode,名称规格统一从这里取 CREATE TABLE Item ( ItemCode TEXT PRIMARY KEY, -- 物料编码,业务主键,不允许重复 ItemName TEXT NOT NULL, Spec TEXT, -- 规格型号,允许为空 Unit TEXT DEFAULT 'PCS', -- 计量单位 ItemType INTEGER DEFAULT 0, -- 0 采购件 1 自制件 2 虚拟件 StdCost NUMERIC DEFAULT 0, -- 标准成本,采购件才有意义 LowLevelCode INTEGER DEFAULT 0, -- 低层码,成本滚算的排序依据 IsActive INTEGER DEFAULT 1 -- 0 停用,停用后新 BOM 不许再选 );

ItemType这个字段决定了展开和成本计算的行为差异,这是新手最容易忽略的地方。采购件是叶子节点,展开到此为止,成本直接取StdCost;自制件要继续往下钻,成本由下层汇总得来;虚拟件(有的厂叫过渡件、工艺件)本身不占库存、不出现在领料单上,展开时要把它的子件直接提到父级下面。把这个类型分清楚,后面所有逻辑都能顺势写下去。

LowLevelCode是另一个关键。同一颗物料可能出现在多个成品、多个层级上,比如一颗螺丝既在整机层用,又在某个模块层用。低层码记录的是这颗物料在全部 BOM 中出现的最深层级,成本滚算必须按低层码从大到小算,否则会出现「上层算完了,下层的成本还没汇总上来」的情况。很多单机版工具让用户手工维护这个字段,我的习惯是加一个批量重算的按钮,每次 BOM 大改后跑一遍,比人工维护靠谱。

编码规则建议在上线前就定死,比如「大类两位 + 流水六位」,别用有意义的编码拼规格尺寸——规格会变,编码不该变。这个规则系统通常不强约束,靠录入约定,所以我在实施时会加一个正则校验,前缀不对直接不让存。

2.2 BOM 头行结构与版本生效

BOM 拆成头和行两层,是这类系统的标准做法。头表记录「谁、哪个版本、什么状态、什么时候生效」,行表记录「用了哪些子件、每个用多少」。为什么要分开?因为版本切换时,变的是整份 BOM,不是某一行。头行分离之后,一个父件可以有 A、B 两个版本并存,A 版作废、B 版生效,历史单据还能追溯到当时的 A 版。

-- BOM 头:一个父件 + 一个版本 = 一份结构 CREATE TABLE BomHeader ( BomId INTEGER PRIMARY KEY AUTOINCREMENT, ParentCode TEXT NOT NULL REFERENCES Item(ItemCode), Version TEXT NOT NULL DEFAULT 'A', Status INTEGER DEFAULT 0, -- 0 草稿 1 生效 2 作废 EffectiveFrom TEXT, -- 生效日期 yyyy-MM-dd Remark TEXT, UNIQUE (ParentCode, Version) -- 同一父件同一版本只能有一份 ); -- BOM 行:用量、废品率、替代料组 CREATE TABLE BomLine ( LineId INTEGER PRIMARY KEY AUTOINCREMENT, BomId INTEGER NOT NULL REFERENCES BomHeader(BomId) ON DELETE CASCADE, LineNo INTEGER NOT NULL, -- 行号,只影响显示顺序 ComponentCode TEXT NOT NULL REFERENCES Item(ItemCode), QtyPer NUMERIC NOT NULL DEFAULT 1, -- 单位父件用量 ScrapRate NUMERIC DEFAULT 0, -- 损耗率,0.02 表示 2% AltGroup TEXT, -- 替代料组号,同组可互换 Remark TEXT ); CREATE INDEX idx_bomline_bom ON BomLine(BomId); CREATE INDEX idx_bomline_comp ON BomLine(ComponentCode);

三个细节值得说。第一,UNIQUE (ParentCode, Version)这条约束必须加上,否则同一版本被录入两次,展开时用量会翻倍,而且这种错误极难肉眼发现。第二,ScrapRate建议存小数而不是百分数,计算时直接1 + ScrapRate,省掉到处除 100 的心智负担。第三,AltGroup替代料组要慎用,替代料在展开时只算一组、不能组内全算,否则成本会虚高。

状态机也要说清楚:草稿可以改行,生效后不能再改,要改就升版;作废版本只读。单机版往往不强制这个流程,用户可以随手改生效版本,这是隐患。我在实施时一般会加一个「生效版本不允许删除行」的触发器,比口头约定管用。

2.3 单机版的能力边界与正确预期

单机版三个字,既是优点也是天花板。优点是数据在本地、一个库文件、拷走就能带走、断网也能用;天花板是没有服务端,权限只能做到「进程序要不要密码」这一层,没有真正的多用户并发控制,也没有审批流。

并发是最容易翻车的地方。两个人同时从共享盘打开同一个库文件,轻则一方保存失败,重则库文件损坏。这类系统的设计前提就是单写入者,要么一个人录完另一个再上,要么用「谁录入谁负责」的分工把写操作集中到一两个人身上。

权限方面,单机版一般是角色级的:管理员能改物料和 BOM,普通用户只能查和打印。做不到「张三只能看 A 产品线」。如果老板一定要这个,正确的做法不是硬改这套系统,而是把库放到一台固定机器上,用远程桌面多人连过去,逻辑上还是单写入。

还有一点预期要摆正:单机版通常不带 MRP 运算和工单管理,它管的是「产品结构」这一层。采购需求、库存扣减、生产排程,得靠别的工具或者人工接。想清楚这条边界,后面二次开发才不会跑偏方向。

3. 目录结构与绿色启动:免安装包解开后长什么样

免安装包最大的好处是不写注册表、不装服务、卸载就是删目录。坏处是很多人拿到就双击 exe,结果报错、或者数据存到了奇怪的地方。这一章按「先看清目录、再启动、最后备份」的顺序走一遍。

3.1 典型目录布局与各文件职责

这类绿色版程序,解压后一般长这样。注意下面描述的是目录职责,不是具体文件清单,不同打包方式会有出入,判断依据看文件名和扩展名。

目录 / 文件职责能不能动
主程序 exe启动入口,双击它不要改名,快捷方式会失效
data 目录放数据库文件,全部业务数据在这只备份,不要改名或移动
config 目录连接串、报表路径、打印设置可以改,改前先复制一份
report 目录报表模板文件可以改,改坏了换回原模板
runtime 目录私有运行库,打包时一起带的删了程序起不来
backup 目录备份输出目录可以清,但别清空最近的
log 目录运行日志可清,出问题时先看这里

判断哪一个是数据库文件,最直接的办法是看扩展名和体积:.db、.mdb、.accdb、.sqlite这类,通常是几百 KB 到几十 MB 不等,而且会随录入增长。找到它,就等于找到了整套系统的命根子。

配置文件的优先级要说一句:程序一般先读 config 里的相对路径,读不到再用内置默认值。所以你把整个目录从 D 盘挪到 E 盘,只要不拆散结构,一般不用改配置。但如果只把 exe 拷到桌面,指向 data 的相对路径就断了,程序会当成首次运行重新建一个空库——这是最常见的「数据丢了」假象。

3.2 首次运行的三步走

第一步,解压到非系统目录。不要放在C:\Program Files下面,那个位置普通用户没有写权限,程序想把数据写在同目录会被系统重定向到用户虚拟目录,看起来能用,实际数据藏在你找不到的地方。我一般统一放D:\Tools\BomTool\这种路径,纯英文、无空格最好。

第二步,补齐运行环境。绿色版免的是「安装」,不是「依赖」。程序如果基于 .NET 写的,机器上得有对应版本的运行时;基于某些编译器的,需要 VC++ 运行库。缺了会弹窗报错,错误信息里一般会写缺哪个。这一步只能按提示装,装完重启一次资源管理器更保险。

第三步,首次启动并确认数据落在哪。启动后随便建一条测试物料,保存,然后去 data 目录看库文件的修改时间是不是刚刚。这一步很多人跳过,等到录了一周数据才发现库文件在别处,追悔莫及。

# 启动前的自检脚本(Windows 下存成 check.bat 放程序根目录) @echo off setlocal set ROOT=%~dp0 if not exist "%ROOT%data" mkdir "%ROOT%data" if not exist "%ROOT%backup" mkdir "%ROOT%backup" echo [1] 检查数据库文件 dir /b "%ROOT%data\*.db" "%ROOT%data\*.mdb" 2>nul if errorlevel 1 echo 未发现数据库,首次启动将自动创建 echo [2] 检查配置目录 if exist "%ROOT%config" (echo 配置目录存在) else (echo 配置目录缺失,可能不是完整包) echo [3] 检查磁盘剩余空间 wmic logicaldisk where "DeviceID='%~d0'" get FreeSpace /value | find "FreeSpace" pause

这段脚本做三件事:确保数据目录和备份目录存在、探测有没有现成的库文件、看一眼磁盘余量。%~dp0是批处理的写法,取脚本所在目录并带尾部反斜杠,这样脚本不管从哪调用,路径都指向程序根目录,不会因为当前工作目录不同而误判。errorlevel判断的是上一条dir的返回码,没匹配到文件就返回非零。最后那句wmic是查盘符剩余空间,老系统上可能被弃用,报错不影响主流程。

3.3 数据备份与迁移的正确姿势

单机版最大的优势在这里:备份就是复制文件。但复制有讲究,程序运行中数据库文件是打开状态,这时直接拷可能拷到一个写到一半的中间态。正确顺序是先退出程序,再复制整个 data 目录,连日志和配置一起打包。

如果不想惊动用户退出程序,用数据库自带的备份语句更稳。SQLite 系的库支持在线备份,导出一个一致性快照;Access 系的可以走压缩修复再复制。下面这个是在线备份的写法,程序开着也能跑。

-- SQLite:在线导出一份完整快照,不影响正在运行的程序 VACUUM INTO 'D:/backup/bom_20250101.db';

VACUUM INTO会在目标路径生成一个干净的新库文件,顺带把碎片整理掉,体积通常比原文件小。目标文件必须不存在,重复执行会报错,所以文件名带上日期,用脚本每天跑一次,保留最近三十份就够。

迁移到新机器更简单:整个目录拷过去,装好运行库,双击。唯一要确认的是配置里如果写了绝对路径(比如报表模板指向D:\report\),新机器上路径不一样就得改。所以我在实施时会把所有路径写成相对于程序根目录,宁可少一点灵活性,换来「拷走就能用」。

4. 核心链路拆解:一次 BOM 展开从点击到结果返回

展开是这套系统的心脏。点一下「展开」,程序要读头、读行、逐层下钻、折算用量、判断虚拟件、防住循环引用,最后把一棵树摊平成表格。这条链路里任何一环写错,结果就是错的,而且错得很隐蔽。

4.1 数据结构与查询入口

展开的起点是拿到「生效版本」。一个父件可能挂着草稿、生效、作废三个版本,只能取状态为生效的那一份。如果同时存在两份生效(约束没加就会这样),取哪一个都是错的,所以前面那条唯一约束不是可选项。

-- 取某个父件当前生效的 BOM 头,多条时按生效日期取最新 SELECT BomId, ParentCode, Version, EffectiveFrom FROM BomHeader WHERE ParentCode = 'P-1001' AND Status = 1 AND (EffectiveFrom IS NULL OR EffectiveFrom <= date('now')) ORDER BY IFNULL(EffectiveFrom, '0000-00-00') DESC LIMIT 1;

date('now')是 SQLite 的取当天日期函数,换成其他库要改成对应写法。IFNULL(EffectiveFrom, ...)处理的是「没填生效日期」的老数据,让它们排在有日期的后面。LIMIT 1是兜底,正常情况下唯一约束已经保证只有一条。

查询要快,索引就得落在(ParentCode, Status)上。单机版数据量通常不大,几万行 BOM 行,但展开是递归调用,每一层都查一次,索引没建好会明显卡顿。

4.2 多层展开与循环引用防护

递归展开有两个写法:数据库里的递归 CTE,和程序里的递归函数。前者一条 SQL 出结果,适合做报表;后者方便加业务判断,适合做界面。我的习惯是两边都写,界面用程序版,导出用 SQL 版,结果对不上就说明有一边写错了。

先看 SQL 版:

-- 递归展开:从 P-1001 往下钻,最多 12 层,path 用于查环 WITH RECURSIVE ex(root, lvl, path, comp, qty_per, accum) AS ( SELECT h.ParentCode, 0, h.ParentCode, h.ParentCode, 1.0, 1.0 FROM BomHeader h WHERE h.ParentCode = 'P-1001' AND h.Status = 1 UNION ALL SELECT ex.root, ex.lvl + 1, ex.path || '>' || l.ComponentCode, l.ComponentCode, l.QtyPer, ex.accum * l.QtyPer * (1 + IFNULL(l.ScrapRate, 0)) FROM ex JOIN BomHeader h ON h.ParentCode = ex.comp AND h.Status = 1 JOIN BomLine l ON l.BomId = h.BomId WHERE ex.lvl < 12 -- 层数上限,兜底 AND instr(ex.path, l.ComponentCode) = 0 -- 已出现在路径上,说明成环 ) SELECT lvl, comp, qty_per, accum FROM ex WHERE lvl > 0 -- 过滤掉第 0 层的根节点 ORDER BY path;

关键的三个参数:lvl < 12是硬性层数上限,防的是数据异常导致无限递归把内存吃光;instr(ex.path, ...) = 0是成环检测,靠把走过的编码拼成一条路径字符串,每下一层就查一次有没有重复;accum是折算到最顶层的累计用量,一单位成品需要多少这个件,全靠它乘出来。

instr有个隐患:如果编码存在包含关系(比如A1和A10),字符串匹配会误判。所以编码规则要么定长,要么在拼接时加分隔符,用'>' || comp || '>'两头都包上更保险。

程序版更灵活,能顺手处理虚拟件穿透:

// 递归展开:虚拟件不占行,但要继续往下钻 public List<BomRow> Explode(string rootCode, decimal rootQty, int maxLevel = 12) { var result = new List<BomRow>(); var path = new HashSet<string> { rootCode }; // 根节点先入路径,防 A→B→A Walk(rootCode, rootQty, 1, maxLevel, path, result); return result; } private void Walk(string parent, decimal parentAccum, int level, int maxLevel, HashSet<string> path, List<BomRow> result) { if (level > maxLevel) return; // 超层直接停,别抛异常打断整棵树 foreach (var line in LoadActiveLines(parent)) // 只取生效版本的行 { if (!path.Add(line.ComponentCode)) { Log.Warn($"检测到循环引用:{string.Join(">", path)}>{line.ComponentCode}"); continue; // 成环跳过并记日志,不中断 } var item = GetItem(line.ComponentCode); // 主数据,取类型和名称 decimal accum = parentAccum * line.QtyPer * (1 + line.ScrapRate); if (item.ItemType != 2) // 虚拟件不写成一行 { result.Add(new BomRow { ParentCode = parent, ComponentCode = line.ComponentCode, Level = level, QtyPer = line.QtyPer, AccumQty = accum }); } if (item.ItemType != 0) // 自制件和虚拟件才继续下钻 Walk(line.ComponentCode, accum, level + 1, maxLevel, path, result); path.Remove(line.ComponentCode); // 回溯,兄弟节点还能用同一个件 } }

path用HashSet而不是List,查重是 O(1),深树展开时差距明显。path.Remove这句别漏,它做的是回溯:兄弟分支可以合法地引用同一颗物料,只有「自己包含自己」才是环。accum在传参时就乘好了,子层拿到的已经是折算到顶层的量,不需要在结果里再乘一次。

LoadActiveLines在实现上要做缓存。同一颗物料在展开过程中可能被访问多次,每次都查库会拖慢速度,我一般用一个字典缓存「编码 → 行列表」,一次展开内有效即可。

4.3 成本滚算与用量折算

有了展开结果,成本就是一道算术题,但有两个坑。

第一个坑:不能把所有层都乘以单价加总。自制件的成本是由它的子件汇总来的,如果你既算了自制件的StdCost,又把它子件的成本加了一遍,那就重复计算了。正确做法是只累加采购件。

第二个坑:顺序。同一颗低层物料被多个上层引用,必须等它所有的上层都汇总完,才能往更上层汇总,这个顺序就是低层码从大到小。

-- 成本滚算:只累加采购件(ItemType=0),避免自制件重复计 WITH RECURSIVE ex(root, comp, accum) AS ( SELECT h.ParentCode, h.ParentCode, 1.0 FROM BomHeader h WHERE h.ParentCode = 'P-1001' AND h.Status = 1 UNION ALL SELECT ex.root, l.ComponentCode, ex.accum * l.QtyPer * (1 + IFNULL(l.ScrapRate, 0)) FROM ex JOIN BomHeader h ON h.ParentCode = ex.comp AND h.Status = 1 JOIN BomLine l ON l.BomId = h.BomId WHERE ex.accum > 0 AND ex.accum < 100000 -- 兜底,防止数据异常放大 ) SELECT ex.root AS 成品, ROUND(SUM(ex.accum * IFNULL(i.StdCost, 0)), 4) AS 材料成本 FROM ex JOIN Item i ON i.ItemCode = ex.comp WHERE i.ItemType = 0 -- 只算采购件 GROUP BY ex.root;

ROUND(..., 4)保四位小数,是因为单价往往带多位小数,用量又是小数,乘出来位数很长,不收敛一下报表会很难看。ex.accum > 0 AND ex.accum < 100000这个区间判断看着粗暴,实际很管用:一旦有人把用量录成 10000,展开结果会瞬间爆炸,这个判断能把它挡在成本计算之外,配合日志能快速定位是哪个件录错了。

如果厂里还要算人工和制造费用,不要在 BOM 层硬塞,另开一张工艺路线表按工序累加,BOM 只管材料。混在一起以后想拆都拆不开。

4.4 反查 Where-used:改一个件影响哪些成品

反查是工程变更时问得最多的问题:这颗电容要停产了,哪些成品受影响?单层反查一条 SQL 就够,多层反查要递归往上走。

-- 单层反查:直接用到 C-2001 的父件 SELECT h.ParentCode, h.Version, l.QtyPer, l.LineNo FROM BomLine l JOIN BomHeader h ON h.BomId = l.BomId WHERE l.ComponentCode = 'C-2001' AND h.Status = 1;

多层反查把条件反过来,从子件往上找父件,递归条件和展开对称。要注意的是,多层反查的结果集可能很大,界面上必须分页,否则一颗通用螺丝能把界面卡死。idx_bomline_comp这个索引就是为反查建的,没有它,几万行数据扫一遍要好几秒。

反查结果建议再叠一层「停用物料」标记:如果某个父件本身已经停用,它其实不算真正的受影响范围,标灰显示比直接过滤更好,让人知道「这个产品已经不做这条线了」。

5. 常见问题排查:绿色版跑不起来的五种典型现场

单机版的问题高度集中,翻来覆去就那几类。下面五条是我在不同机器上反复遇到的,按「现象、原因、解决」记下来,下次遇到直接对号入座。

5.1 双击没反应,或者弹窗提示缺少运行库

现象:双击主程序,鼠标转两圈就没了,任务管理器里也看不到进程;或者弹一个框,写着找不到某个 dll、某个框架版本。

原因:绿色版打包的是程序本身,不带运行环境。机器上缺对应版本的运行时,进程启动瞬间就退出,连日志都来不及写。另一种情况是系统位数不匹配,32 位程序在纯 64 位环境里缺 32 位兼容组件。

解决:先看弹窗里写的缺什么,按名字去装对应运行时,装完重启一次。如果连弹窗都没有,用命令行在程序目录下执行 exe,错误信息会打在控制台里,比双击多很多线索。装运行库时注意版本要对上,装了高版本不一定向下兼容。

5.2 录入的数据关掉程序就没了

现象:录了一上午物料,正常关闭,下午打开发现全空了,库文件还在,但里面没数据。

原因:九成是程序把数据写到了别处。典型场景是把 exe 单独拷到桌面运行,程序找不到相对路径下的 data 目录,就在用户目录下新建了一个空库;你看到的是新库,老数据还在原来的目录里躺着。

解决:先别慌,全盘搜一下同名数据库文件,按修改时间排序,最近改过的那个多半就是真身。找到后把整个程序目录恢复成完整结构,再启动。要根治,就在启动脚本里强制切到程序目录,别依赖当前工作目录。

# 启动包装脚本:强制切到程序所在目录再启动,避免相对路径错位 @echo off cd /d "%~dp0" start "" "BomTool.exe"

cd /d的/d参数是为了跨盘符切换,%~dp0取脚本目录。用户以后就点这个 bat,不点 exe,路径错位的问题基本绝迹。

5.3 放到共享盘上,两个人同时录入就出错

现象:库文件放共享目录,两个人同时开程序,一方保存时报「数据库被锁定」,或者更糟,保存成功了但数据错乱,某条 BOM 行凭空多出来一条。

原因:单机版的库是文件级锁,没有服务端做事务排队。两个进程同时写,谁先谁后全看运气,锁竞争失败就报错,锁粒度没控制好就写坏。

解决:明确这条使用纪律,单机版就是单写入。要么指定一个人专门录入,其他人只读;要么把程序装在一台固定机器上,其他人远程连过去操作,物理上只有一个进程在写。真要多人并发,得换带服务端的版本,别在文件库上硬扛。

5.4 展开结果翻倍,或者直接报错中断

现象:某个成品的展开明细里,同一颗物料出现两次,用量正好是两倍;或者点展开直接弹异常,日志里写着递归层数超限。

原因:翻倍一般是同一父件存在两份生效 BOM,两条头记录都通过了状态筛选,行被重复读取。报错中断多半是循环引用:A 的 BOM 里有 B,B 的 BOM 里又有 A,递归没有出口。

解决:先跑一句查重的 SQL,看有没有重复生效的头。

-- 排查重复生效的 BOM 头 SELECT ParentCode, COUNT(*) AS 生效份数 FROM BomHeader WHERE Status = 1 GROUP BY ParentCode HAVING COUNT(*) > 1;

查出来就把多余的作废,只留一份。循环引用用第 4 章那段路径检测就能定位,日志里会打出完整的环路径,顺着路径改数据即可。根治办法是在保存 BOM 行时做一次「祖先检查」,把成环的数据挡在入库之前。

5.5 导出 Excel 中文乱码,或者日期变成一串数字

现象:导出的 CSV 用 Excel 打开,中文全是问号或方块;导出的日期列显示成 45231 这种数字。

原因:CSV 默认按 ANSI 编码写,Excel 打开时按本地编码猜,猜错就乱码;日期如果在库里存的是序列号,导出时没格式化,Excel 就按数字显示。

解决:导出 CSV 时在文件头写 UTF-8 BOM,Excel 认这个标记。日期在 SQL 里就格式化成字符串再导出,别把原始类型丢出去。

-- 导出时直接把日期格式化成可读字符串 SELECT ItemCode, ItemName, strftime('%Y-%m-%d', CreatedAt) AS 创建日期 -- SQLite 写法 FROM Item WHERE IsActive = 1;

BOM 头那三个字节(EF BB BF)很多导出库有开关,找不到就自己写文件时先写进去。日期格式化放在 SQL 层比在程序层处理好,少一层转换少一处出错。

6. 二次开发:在单机版上长出自己厂里的规矩

这类系统用半年,一定会冒出「能不能加个字段」的需求。最蠢的做法是直接改原表加列,改完升级一次全丢。我一般走扩展表的路子:主表不动,另开一张键值表挂扩展字段,界面读的时候做一次左连接。

-- 扩展字段表:不动主表结构,升级时不受影响 CREATE TABLE ItemUdf ( ItemCode TEXT NOT NULL, FieldKey TEXT NOT NULL, -- 字段名,如 '供应商料号' FieldValue TEXT, PRIMARY KEY (ItemCode, FieldKey) ); -- 视图把扩展字段摊平成列,界面直接查视图 CREATE VIEW vItemFull AS SELECT i.*, MAX(CASE WHEN u.FieldKey = '供应商料号' THEN u.FieldValue END) AS 供应商料号, MAX(CASE WHEN u.FieldKey = '安全库存' THEN u.FieldValue END) AS 安全库存 FROM Item i LEFT JOIN ItemUdf u ON u.ItemCode = i.ItemCode GROUP BY i.ItemCode;

MAX(CASE WHEN ...)这个写法是行转列的标准套路,一个字段一列,加字段只改视图不改表。代价是字段多了以后视图会变长,几十个字段以内完全没问题。

第二个常改的地方是校验。BOM 数据烂在源头,后面所有报表都是错的。我习惯在批量导入或保存前跑一遍体检脚本,把问题一次性列出来,而不是等展开报错才去查。

import sqlite3 from collections import defaultdict conn = sqlite3.connect(r"D:\Tools\BomTool\data\bom.db") cur = conn.cursor() # 1) 收集所有生效 BOM 的父子边 edges = defaultdict(list) for parent, comp in cur.execute(""" SELECT h.ParentCode, l.ComponentCode FROM BomHeader h JOIN BomLine l ON l.BomId = h.BomId WHERE h.Status = 1 """): edges[parent].append(comp) # 2) 深搜找环,打印完整环路径 def dfs(node, stack, seen): for child in edges.get(node, []): if child in stack: print("发现环:", " -> ".join(stack[stack.index(child):] + [child])) continue if child in seen: continue seen.add(child) dfs(child, stack + [child], seen) for n in list(edges): dfs(n, [n], set()) # 3) 用量为 0 或负数的行,几乎都是录错 for row in cur.execute("SELECT BomId, LineNo, ComponentCode, QtyPer FROM BomLine WHERE QtyPer <= 0"): print("异常用量:", row) conn.close()

stack记录当前路径,seen记录这一轮已访问过的节点,两者分工不同:stack负责定位环,seen负责剪枝避免重复遍历。这个脚本我一般放在导入流程的末尾,跑完看输出,有问题当场改。

改造完怎么验证?我有一张固定的清单:造三个测试成品,一个三层结构、一个带虚拟件、一个带替代料组,改完代码就把这三个跑一遍,手工算好标准答案对账。对不上就先怀疑代码,别急着怀疑数据。

改造点改动位置风险
加自定义字段建 Udf 表 + 改视图视图字段名和界面绑定要同步
改成本算法成本 SQL 或滚算函数自制件重复计算是最常见的错
加导出格式导出模块编码和日期格式两处必查
加校验规则保存前钩子别把老数据挡在门外,先跑存量体检

最后说个教训。早年我图省事,直接在主表上加了两列,用了小半年都挺好,直到换了一台机器重装程序,新版本的安装逻辑把表结构重建了一遍,那两列连同数据一起没了,补了一整天才补回来。从那以后我每次动数据库,都强制先跑一遍「结构差异比对」,先确认改的是扩展表还是主表,再动手。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进