
简介一套面向电子制造与硬件研发场景的开源物料清单BOM管理系统的C#示例工程用于在集中式数据库中管理电子零件与物料清单数据支持多用户并行协作和版本控制并与Ciiva电子组件搜索API深度集成。压缩包共50个文件以37个.cs源码文件为核心涵盖Ciiva.Api.Dto接口DTO定义与ApiDemo演示项目另有5个dll、项目文件csproj、配置文件、图标及资源文件等整体仅315KB便于快速浏览。已有838人学习适合需要构建BOM管理工具或元器件选型功能的.NET开发者。通过这组代码可以掌握按零件号查询供应商、获取库存与价格、订阅状态、汇率等典型API的请求响应模型并借鉴多供应商结果封装和C#桌面端调用示例降低与第三方元件数据源对接的门槛。1. BOM Management Software 就是一个简单的「Excel 物料清单数据库化」方案它适合谁、解决什么给你说个反直觉的现象BOM 混乱的团队往往不缺 Excel 高手缺的是一个把 Excel 收口的地方。BOM Management Software 这个开源项目做的事情很直白——把散落在工程师、采购员、工艺员电脑里的 Excel 物料清单BOM统一收进一个集中式数据库再通过导入、版本、权限、导出这一条链把数据管起来。它不是要取代 PLM而是先帮你把「到底哪个 Excel 是最新的」这个问题解决掉。适合三类人一类是还在用 Excel 维护 BOM、被多版本协作折磨的工程师一类是打算上 PLM 但眼下需要一个过渡方案的工艺和 IT 人员还有一类是想研究 BOM 数据模型、打算自己二次开发的开源技术爱好者。它不会让 BOM 管理一夜之间变完美但会把「数据在哪里」「谁改过」「当前版本是什么」这三件最基础的事变成数据库里的确定答案。2. BOM 进库前的准备工作物料主数据、层级与单位怎么建模2.1 Excel BOM 最大的问题不是格式是「同一件事有多个版本」只要做过几年产品你一定见过这种场景设计部导出的装配树是一份 Excel采购部手里又有一份带价格的 BOM工艺部再添一列加工工时流程部复制了一版改成「已发布」。同一个物料号在不同文件里描述字段不一样同一个父件在不同 Excel 里子件数量不一样。大家都有理但没人知道谁是基准。把 Excel 里的 BOM 收进集中式数据库本质不是在「存文件」而是在给数据建立一个唯一的公认副本。数据库作为唯一数据源Excel 只承担两种角色导入时的源文件和导出时的交付格式。这样就把「数据管理」和「文件传递」两件事分开了。很多从 Catia 二次开发导出的装配树、OrCAD 原理图导出的 BOM 表抓过来一看都是同样的乱象——表头不统一、层级靠缩进表示、同样的物料在不同行重复出现。让数据库先接手再考虑怎么把格式理顺。2.2 物料主数据先把物料号稳住我在给团队落地这个方向时第一件事不是建 BOM 表而是建物料主数据表。原因很实际如果物料号都不稳定BOM 关系再正确也是空中楼阁。物料号才是关联的锚点物料名称、规格、描述都只能作为辅助信息。常见的设计是拆成三张表物料主数据表、BOM 头表、BOM 行表。下面是一个典型的建表思路-- 物料主数据一个物料号只允许出现一次 CREATE TABLE material ( material_code VARCHAR(50) PRIMARY KEY, description TEXT NOT NULL, spec VARCHAR(100), default_uom VARCHAR(10), is_disabled BOOLEAN DEFAULT FALSE ); -- BOM 头一条记录代表一个父件的一个版本 CREATE TABLE bom_header ( bom_id SERIAL PRIMARY KEY, parent_material_code VARCHAR(50) REFERENCES material(material_code), site VARCHAR(20), revision VARCHAR(20), status VARCHAR(20) DEFAULT draft, effective_date DATE, created_by VARCHAR(50), created_at TIMESTAMP DEFAULT now() ); -- BOM 行父件下面的所有子件明细 CREATE TABLE bom_line ( line_id SERIAL PRIMARY KEY, bom_id INT REFERENCES bom_header(bom_id), line_no INT, child_material_code VARCHAR(50), qty_per NUMERIC(18,6), uom VARCHAR(10), substitute_group VARCHAR(20), scrap_rate NUMERIC(5,2) DEFAULT 0 );这段 DDL 里值得注意的参数有三个。material_code设为主键是强迫数据在源头收敛qty_per用 NUMERIC 而不是 FLOAT是因为用量是业务数据浮点数在后续展开计算时会出现 1.0000000000002 这种莫名其妙的差异status字段存草稿、已发布、已失效等状态是后面做版本管理的依据。实际项目里还可以加工厂、业务类型、替代组等字段但在这个最小模型里已经能把 BOM 的「骨架」撑起来了。2.3 BOM 的两种模型单层存储与多级展开Excel 里的 BOM 通常是一个树状结构父件下面有子件子件本身又可能是一个组件再往下还有下一层。很多人第一次做数据库化时会犯一个错在 Excel 里怎么缩进数据库里就怎么存层级每一级都带一个 level 字段。这种设计看着直观但维护起来想死——因为一旦中间某层改了父子关系所有下级记录的 level 都要跟着改漏改一处就全乱了。常见做法是「单层存储 多级展开」数据库里每条 BOM 行只存直接的父子关系。比如 Arm 组件的 BOM 行里直接子件是 Arm_Left 和 Arm_Right而 Arm_Left 在另一条 BOM 头下再展开。查询时用递归把整棵树拼出来。做物料需求计算时从某个父件出发一层一层往下查直到所有节点都是外购件为止。这个模型的好处是数据的一致性由外键保证坏处是需要写递归查询而且必须设置递归深度上限防止 A 包含 B、B 又包含 A 的环路把查询拖死。2.4 单位与数量件/套/公斤要怎么归一Excel BOM 里的单位一直是重灾区零件用「件」螺丝用「个」线缆用「米」胶水用「公斤」还有更过分的「套」——一套本身是个合集没人说得清下一层是什么。导入集中式数据库时单位不做归一后面算采购需求时一定会翻车。我一般会在系统里加一个单位表每个物料默认单位挂在物料主数据上BOM 行里的单位可以覆盖默认单位CREATE TABLE uom ( uom_code VARCHAR(10) PRIMARY KEY, uom_name VARCHAR(50), base_uom VARCHAR(10), conversion_factor NUMERIC(18,6) ); INSERT INTO uom VALUES (pcs, 件, pcs, 1); INSERT INTO uom VALUES (kg, 公斤, kg, 1); INSERT INTO uom VALUES (meter, 米, meter, 1);这里有个关键参数conversion_factor统一往基准单位换算。比如「套」不能直接定义成一个固定系数因为它包含的物料明细可能变化。处理办法是只有「套」作为虚拟包装单位出现在 BOM 行上不参与数量换算真正核算需求时按下一层展开后的明细数量来算。这也是为什么很多系统要求「同一种物料的单位必须在整个 BOM 树中保持一致」不是为了管的严而是为了避免 1 公斤胶水和 1 升胶水在同一个配方里被当成同一个数。3. 导入 Excel 到集中式数据库最小可跑通的步骤与关键参数3.1 方案先把 Excel 洗成导入模板再让数据库去接拿到一堆 Excel 直接往数据库灌第一步就会卡住——因为每个工程师导出的 BOM 都不一样。有人从 ERP 系统导出有表头有人从设计软件直接保存前面还带着几行项目信息还有人把明细放在第二个 sheet第一个 sheet 里是封面图。所以我不建议把 Excel 当作数据库的直接输入而是先做一个清洗步骤把各种乱七八糟的 Excel 转成一个标准导入模板再用脚本批量加载进数据库。这个清洗步骤可以是 Python 脚本也可以是一份预设了字段的 Excel 模板。我的经验是小团队用模板就够但如果 Excel 来源多、格式不统一脚本清洗更可靠。而且用脚本处理有一个额外好处——不需要在客户端装任何 Excel 加载项。项目上线后你永远不知道用户机器上的加载项什么时候被禁用一旦被禁用整个导入流程就断了。用 pandas 读取 Excel 文件是独立进程跟 Excel 应用本身互不干扰。3.2 用 Python 把 Excel 转成 CSV前导零、合并单元格、列名下面这个脚本是一个典型的最小清洗流程。它做的事情是定位表头行、重读数据、把物料号统一成字符串防止前导零丢失、拆分多级缩进最后输出标准 CSVimport pandas as pd raw pd.read_excel(assembly_bom.xlsx, sheet_name0, headerNone) # 快速定位表头行很多导出文件的头部有多行注释不能假设表头在第 0 行 header_row None for i, row in raw.iterrows(): cell str(row.iloc[0]).strip().upper() if cell 父件编码 or cell PARENT_ITEM: header_row i break if header_row is None: raise ValueError(找不到表头行请确认列名是 父件编码 或 PARENT_ITEM) bom pd.read_excel(assembly_bom.xlsx, sheet_name0, headerheader_row) # 物料号统一转字符串并去除空格 for col in [父件编码, 子件编码]: bom[col] bom[col].astype(str).str.strip() # 只对纯数字物料号补前导零 mask bom[col].str.match(r^\d$) bom.loc[mask, col] 0 bom.loc[mask, col] # 数量列统一转为数值空值填 0 并记录警告 bom[数量] pd.to_numeric(bom[数量], errorscoerce).fillna(0) # 单位列空值用父件的默认单位补 bom[单位] bom[单位].fillna(bom[默认单位]) # 输出标准模板后续加载进数据库 bom.to_csv(bom_import_ready.csv, indexFalse, encodingutf-8-sig)两个关键点要说明。一是前导零问题。很多物料号是「0012345」这种含前导零的编码Excel 单元格一旦被识别成数字就会存成 12345。如果数据库里原始物料号带零而导入文件里不带零关联就会失败。这里用str.match(r^\d$)只对纯数字物料号补零而不是对所有物料号处理避免把「ABC-01」这种字符串改坏。二是encodingutf-8-sig。如果不加 BOM 头导出的 CSV 用 Excel 打开时中文会乱码。这个问题在 Windows 机器上尤其常见别问我是怎么知道的——乱码这个问题本身是玄学但只要带上-sig后缀基本就稳了。3.3 容器化部署用 Docker Compose 把数据库和服务拉起来清洗好的 CSV 怎么进数据库如果这个开源项目本身提供了 Web 界面那就用界面导入如果是纯后端项目就需要手动写导入脚本。我通常会把数据库和后端服务用 Docker Compose 一起起方便本地复现和测试。一个最小的容器编排文件是这样的services: db: image: postgres:16 environment: POSTGRES_DB: bom_mgmt POSTGRES_USER: bom POSTGRES_PASSWORD: bom_password ports: - 5432:5432 volumes: - bom_data:/var/lib/postgresql/data app: build: . depends_on: - db environment: DB_URL: postgres://bom:bom_passworddb:5432/bom_mgmt BOM_IMPORT_DIR: /data/import volumes: - ./import:/data/import ports: - 8080:8080 volumes: bom_data:这段配置的核心参数是DB_URL、BOM_IMPORT_DIR和depends_on。depends_on保证数据库先启动但注意它只能控制容器顺序不能保证数据库已经做好接受连接的准备——所以应用代码里通常还需要一个重启重试逻辑。BOM_IMPORT_DIR把本机的./import目录挂载进容器你把清洗好的 CSV 丢到这个目录里应用就能读到。这里有个常见坑PostgreSQL 的默认端口 5432 很容易被本机已有的数据库占用。如果启动报端口冲突把左边那个5432:5432改成15432:5432就能绕开。右边是容器内端口不要动。3.4 导入时的三个关键参数唯一键、递归深度、批量大小把清洗好的数据实际加载进 BOM 表时有三个参数需要反复确认我每次做导入都盯着它们。第一个参数是唯一键的判定依据。BOM 行的唯一键并不是「某个物料号」本身而是「父件编码 BOM 版本 行号」。因为同一个父件可以有多个版本同一个版本下也可能有多个相同子件分布在不同的行。如果不按这个组合去重导入就会出现主键冲突或者重复数据。第二个参数是递归深度。做多级展开时如果你发现某个 BOM 查询非常慢第一反应应该是检查是否有循环嵌套。A 包含 BB 包含 A这在逻辑上是错的但 Excel 数据里真的会出现。导入完成后跑一次展开如果递归深度超过 10 层就要警惕超过 20 层基本是环路。很多项目的导入参数里就带一个max_depth默认 10超过就报错——宁可让导入失败也不要把脏数据放进去。第三个参数是批量大小。一次性把几万行 BOM 明细插入数据库事务日志会非常大失败回滚也慢。常见做法是每 5001000 行提交一次。这个值不是越大越好因为单次事务超过一定规模后行锁竞争和 wal 日志写入都会拖慢速度。我一般先用 500 行起步性能瓶颈明显的再往上调。4. 导入之后的日常版本管理、权限、下载与集成4.1 版本管理新增、修订、替代料的处理流程数据进了数据库只是完成了第一公里。真正拉开差距的是版本管理。Excel 时代大家靠文件名区分版本「BOM_v5_最终版.xlsx」「BOM_v5_最终版_改3.xlsx」这种文件名我见得太多了。在集中式数据库里版本不应该靠文件名区分而应该靠bom_header表里的revision和status字段控制。一个典型的流程是先创建新版本的 BOM 头状态为草稿从旧的 Excel 或旧版本导入明细修改若干行后提交为已发布状态。已发布的版本不允许直接修改明细只能新建下一个修订版本。这就像给数据上了道锁——你不能在发布之后偷偷改数据改必须是显式的新版本。这样做最大的价值是给了团队一颗后悔药就算新版本数据有误也能随时切回上一个已发布版本。版本之间还有个常见问题就是替代料怎么处理。同一个物料在 BOM 行里的substitute_group字段相同就代表这几个物料可以互相替代。但替代关系不能跨版本继承因为某个版本可能就是因为替代料变更才升版的。所以每次新建版本时替换组要人工复核不能直接照搬旧版本。4.2 权限谁可以改谁只能看谁负责发布BOM 是制造业数据里最牵一发动全身的东西。物料清单一旦错了采购会多买一堆废料产线会装不上件售后会找不到备件。所以权限设计不能省不要等出了问题才想起来补。我在落地时用的是最基础的三角色模型角色能做的操作默认不能做的操作查看者查看已发布的 BOM、导出 Excel不能改数据、不能看到草稿版本工程师创建草稿版本、编辑 BOM 明细、上传 Excel 导入不能发布、不能删除已发布的版本审批人查看所有版本、发布/撤回版本、停用物料不能直接编辑明细改动必须通过工程师这个模型不复杂但足够挡住 90% 的误操作。有了权限体系之后Excel 时代常见的「谁都能改一版然后群发给大家」就会得到收敛。注意不要给工程师开「发布」权限这一步要留给审批人流程上走一个闭环。否则导入的时候一个手滑错的数据就带着「已发布」标签扩散出去了。4.3 下载与集成导回 Excel 或直接给 PLM/ERP 提供 API集中式数据库的反面是「系统里的人能用外面的人还在要 Excel」。所以导入之后必须有一个导出能力。导出的核心诉求是两点列名保持稳定内容保证和数据库一致。下面是一个把已发布 BOM 导回 Excel 的 Python 示例import pandas as pd import psycopg2 conn psycopg2.connect(dbnamebom_mgmt userbom hostlocalhost) sql SELECT h.parent_material_code AS 父件编码, l.line_no AS 行号, l.child_material_code AS 子件编码, l.qty_per AS 数量, l.uom AS 单位, l.substitute_group AS 替代组 FROM bom_header h JOIN bom_line l ON l.bom_id h.bom_id WHERE h.revision %s AND h.status released ORDER BY l.line_no; df pd.read_sql(sql, conn, params[BOM-REV-002]) df.to_excel(bom_rev002_export.xlsx, indexFalse)这个导出脚本里有个细节用params传版本号而不是直接拼字符串这是为了防止 SQL 注入同时避免版本号里带特殊字符把语句打断。导出的文件列名是中文因为工程团队的交付习惯是用中文表头交接给生产或采购时能直接打开看。如果你对接的下游系统是 ERP一般要导出成固定列名、固定编码格式的 CSV不要用 Excel因为 ERP 的导入程序大多只认 CSV。如果你要对接 PLM 或 ERP 系统常见做法是让这个集中式数据库提供一个只读 HTTP 接口下游系统通过物料号和版本号来查询 BOM而不是去直接读数据库。接口比直连数据库安全得多也方便做权限控制和日志审计。BOM Management Software 这类开源项目通常会自带基础 REST API如果没有用 FastAPI 包一层也很简单。5. BOM 导入常见问题排查现象、原因和解决办法5.1 合并单元格导致父件行号错位现象导入后同一父件的所有子件数量翻倍或者某些行被错误地归到了上一个父件下面。原因原始 Excel 里的父件编码列做了单元格合并。Excel 里合并单元格时只有左上角那个单元格有值其余行的父件编码都是空。用 pandas 读取时空值不会自动向下填充于是这些行的父件编码就丢了导入逻辑把前一行非空父件编码带下来造成错位。解决在清洗脚本里加一个ffill操作把父件编码列的空值填充成上一个非空值。注意只对父件编码做不能对所有列做否则数量列的空值也会被错误填充。5.2 物料号前导零丢失现象数据库中显示物料号是 12345但原始 Excel 里是 0012345导致 BOM 关联或查询查不到。原因Excel 打开 CSV 时自动把纯数字物料号识别成数值类型前导零被丢弃。数据进入数据库时已经是 12345源头错误了后面怎么查都不对。解决清洗阶段强制把物料号列转成字符串并且对纯数字物料号补足固定位数。更彻底的办法是导出的 Excel 模板里把物料号这一列的单元格格式设为文本。如果物料号已经在数据库里丢零了只能导出一份正确的物料主数据表重新灌录一次没有后悔药可吃。5.3 单位混乱换算翻车现象采购算出来的需求数量比实际多了 10 倍或者某些物料数量异常大明显不合理。原因同一物料在 BOM 不同层级上用了不同单位。比如电机在装配件里按「台」算在油漆工序里按「公斤」算展开到最底层时系统误把单位不同的值直接相加。解决在导入检查规则里加一道约束——同一物料在整个 BOM 树里只允许出现一个默认单位BOM 行里允许覆盖但覆盖单位必须在单位表里能换算回默认单位。如果换算因子缺失导入就报错而不是默默接受。5.4 Excel 列名大小写导致字段映射为空现象导入列表显示某些列 100% 为空但打开 Excel 一看明明有数据。原因有的 Excel 文件表头是「子件编码」有的导出工具输出的是「子件编码」加了全角空格有的同行发给你的文件里列名末尾多了一个不可见字符。脚本按精确列名去找找不到就返回空。解决读取表头后统一做一次归一化——去首尾空格、把全角括号转半角、把列名都转成小写再比较。这个规则看起来卑微但能挡住一大半「很奇怪」的导入异常。Excel 快速定位到某个空列对应哪一行的技巧是利用 pandas 的df[df[col].isna()]拿到索引后再回原文件核对比人眼在一千行里翻快得多。5.5 大批量导入卡死与递归展开爆栈现象导入几千行的 BOM 明细时进度条一直不动或者展开多层 BOM 时程序直接报递归层数超限。原因大批量导入时每一行明细都独立打开一个事务连接池耗尽递归展开时没有设置最大深度出现了循环引用或者异常深的多级装配。解决导入改为批量分批提交每 500 行一个事务递归展开统一加max_depth超过 10 层断开并记录警告日志。实际项目里正常的装配深度很少超过 8 层10 层已经是安全值。6. 验证与进阶写一个对账脚本把导入结果拿回来看导入完成不等于数据正确这是我在这个项目上最深的体会。常规做法是把数据库里的已发布 BOM 导出一份展开明细再拿一个独立的解析程序重新读一遍原始 Excel两边做对比。这个对账步骤不需要每次都人工触发做成脚本以后每次导入后跑一遍就行。下面是我常用的一个校验思路import pandas as pd # 第一步重新解析原始 Excel重新计算总行数和总数量 raw pd.read_excel(assembly_bom.xlsx, header0) src_count len(raw) src_qty raw[数量].sum() # 第二步读取数据库导出的结果 db pd.read_excel(bom_rev002_export.xlsx) db_count len(db) db_qty db[数量].sum() # 第三步核心字段做集合比对 src_pairs set(zip(raw[父件编码], raw[子件编码])) db_pairs set(zip(db[父件编码], db[子件编码])) missing src_pairs - db_pairs extra db_pairs - src_pairs if missing or extra: print(f有差异: 缺失 {len(missing)} 项, 多余 {len(extra)} 项) else: print(BOM 明细一致)这套校验逻辑虽然朴素但非常管用。它把「看起来没问题」变成「数字对得上」。跑对账的时候我一般会比对五个维度数据总行数、数量总和、父件子件组合、物料号集合、单位取值分布。如果五个维度都一致导入基本可以放心。再进阶一步这个脚本可以做成定时任务。每天夜里从数据库拉一次导出跟上游系统生成的 Excel 对一遍第二天早上看一眼报告就能知道有没有数据漂移。我现在养成的习惯是导入之前先跑一遍对账脚本导入之后随手再跑一遍遇到不一致先查 Excel 而不是先改数据库。这一条经验帮我避免过好几次把对的改成错的希望帮到你。本文还有配套的精品资源点击获取