尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

开源ERP库存管理系统实战:从选型到Spring Boot实现与并发控制

开源ERP库存管理系统实战:从选型到Spring Boot实现与并发控制 先聊聊我最近的感受。很多中小型制造企业和贸易公司在库存管理上其实一直处于“半手工”状态采购靠Excel登记销售开单靠聊天记录仓库盘点靠人肉数数。一开始订单量小还没感觉等业务跑起来之后库存账面和实物对不上、订单超卖、采购重复下单等问题就会接连出现。企业想直接上SAP、Oracle这类商业ERP成本太高实施周期也长。这时候开源免费的ERP库存管理系统就成了一个性价比很高的选择。市面上确实有不少成熟的开源项目也有不少团队选择基于开源框架二次开发一套自己的库存管理系统。这篇文章我就从开源ERP库存管理系统的选型思路、核心概念、数据库设计、接口实现到常见问题和工程化建议做一次完整的拆解帮助你从零搭建一套可用的库存管理系统或者帮助你评估现有开源项目的落地方式。文章会包含大量可复用的SQL、Java和Vue代码重点讲清楚库存系统的核心闭环入库、出库、库存流水、库存余额以及并发扣减库存时的安全问题。不管你是准备学习ERP开发还是要在企业内部推动开源ERP落地这篇文章都值得收藏备用。1. 开源ERP库存管理系统是什么解决什么问题1.1 什么是ERP库存管理系统ERPEnterprise Resource Planning是企业资源计划系统的简称它覆盖财务、采购、销售、生产、库存、人力资源等模块。而库存管理系统通常指其中负责物料管理和库存核算的部分它要回答几个核心问题仓库里现在有什么物料每个物料的数量是多少这些物料分布在哪些仓库库存的变化是由哪张业务单据触发的当前的可用库存是否能满足一张销售订单所以库存管理系统并不是简单的“进销存登记表”。它必须把采购入库、销售出库、生产领料、退货、盘点、调拨这些业务动作统一建模形成一条完整的数据链路。1.2 开源ERP与传统商业ERP的区别对比维度开源ERP商业ERP软件许可费用免费或低成本按模块、用户数、年费计费源代码通常开放可二次开发闭源定制依赖原厂实施方式自研团队或第三方实施原厂或授权顾问实施扩展能力可灵活修改业务逻辑受限于平台能力技术门槛较高需要懂开发和运维较低配置为主典型代表Odoo、ERPNext、Apache OFBizSAP、Oracle EBS、用友、金蝶选择开源ERP库存管理系统并不意味着“免费所以凑合”。相反你需要具备一定的技术能力去掌控它。很多企业选择开源方案核心诉求是三点源代码可控业务逻辑可以按需改造。无License授权压力尤其是对预算敏感的成长型企业。技术栈公开透明不绑定特定厂商。1.3 常见的开源ERP项目概览项目名称技术栈适用场景许可证OdooPython PostgreSQL中小型制造、贸易、电商LGPLERPNextPython Frappe框架中小企业全模块管理GPLApache OFBizJava Tomcat大型企业复杂业务Apache 2.0华夏ERP基于SpringBootJava Vue MySQL国内中小企业进销存GPL若依RuoYiJava Vue适合二次开发后台管理系统MIT需要说明的是开源项目的许可证非常关键。如果企业计划对系统做闭源二次发行那么GPL类协议会有较严格的约束如果是内部使用不对外发行通常影响较小。做技术选型时一定要去对应开源仓库确认License原文不能只看社区传闻。1.4 开源库存管理系统适合哪些场景年营收在几千万到几亿之间、还在用Excel管理库存的中小制造企业。有技术团队、希望自己掌控核心业务系统的成长型公司。高校、实验室、非营利组织预算有限但又需要规范流程管理。个人开发者学习ERP业务逻辑接触真实进销存场景。软件公司希望基于成熟开源框架快速交付客户项目。2. 开源ERP库存管理系统的技术选型与架构设计2.1 技术栈选型建议在实际项目中库存管理系统通常作为整个ERP系统中的子模块出现。如果从零开始搭建一套轻量级库存管理系统我比较推荐下面的技术组合技术层次推荐方案说明前端框架Vue 3 Element Plus组件生态丰富适合中后台系统后端框架Spring Boot 2.x / 3.xJava生态成熟事务与并发控制能力强数据库MySQL 8.0使用广泛运维成本低权限框架Spring Security JWT实现登录认证与接口权限构建工具Maven / npm标准工程构建方式部署方式Docker Compose便于本地环境与生产环境保持一致这里的版本号只是推荐具体需要结合你的项目要求来定。Spring Boot 3.x基于Jakarta EE与Spring Boot 2.x在某些依赖上不兼容MySQL 8.0支持窗口函数和更好的锁机制但如果你已有的生产环境是5.7也不必强行升级。本文侧重讲解库存管理系统的核心设计代码示例会尽量保持通用。2.2 整体架构图文字版Vue 3 Element Plus | | HTTP/JSON v Spring Boot 后端服务 |--- 认证模块 (JWT) |--- 商品管理模块 |--- 仓库管理模块 |--- 入库单模块 |--- 出库单模块 |--- 库存流水模块 |--- 库存余额模块 | | JDBC v MySQL 数据库 |--- sys_user 用户表 |--- bas_product 商品表 |--- bas_warehouse 仓库表 |--- stk_inbound 入库单表 |--- stk_inbound_item 入库明细表 |--- stk_outbound 出库单表 |--- stk_outbound_item 出库明细表 |--- stk_stock_flow 库存流水表 |--- stk_stock_balance 库存余额表从整体架构可以看出库存管理系统的核心是“单据 流水 余额”三层模型。业务操作不直接修改库存数量而是通过生成入库单或出库单同时记录库存流水最后更新库存余额。2.3 核心业务闭环库存系统的完整闭环可以拆成下面几步用户创建入库单填写供应商、收货仓库、商品和数量。入库单审核通过后系统生成入库库存流水。系统更新对应仓库、对应商品的库存余额。用户创建出库单填写客户、发货仓库、商品和数量。出库单审核时系统检查可用库存是否充足。库存充足则扣减库存不足则提示库存不足。财务或管理人员通过报表查看库存变动、库存价值、出入库趋势。这个闭环看起来简单但实际开发中并发扣减库存、事务一致性、幂等性等问题非常容易出现。3. 数据库表设计库存系统的基石3.1 数据模型设计原则在设计库存管理系统数据库时至少要遵循以下原则单据与明细分离主表存单据头信息明细表存商品明细避免单表字段过多。数量与金额精度控制库存数量使用DECIMAL不使用浮点类型避免精度丢失。库存流水只增不改流水表通过流水号唯一约束一旦写入不允许修改和删除只能通过冲销单纠正。库存余额必须有版本号或锁机制用于处理并发更新防止超卖。所有业务表都要带创建人、创建时间、更新人、更新时间和逻辑删除标记。3.2 商品表和仓库表-- 商品表 CREATE TABLE bas_product ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, product_code VARCHAR(50) NOT NULL COMMENT 商品编码, product_name VARCHAR(100) NOT NULL COMMENT 商品名称, category_id BIGINT COMMENT 分类ID, unit VARCHAR(20) DEFAULT 件 COMMENT 单位, spec VARCHAR(100) COMMENT 规格型号, status TINYINT DEFAULT 1 COMMENT 状态1启用0停用, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除0否1是, create_by VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_by VARCHAR(50), update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_product_code (product_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表; -- 仓库表 CREATE TABLE bas_warehouse ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, warehouse_code VARCHAR(50) NOT NULL COMMENT 仓库编码, warehouse_name VARCHAR(100) NOT NULL COMMENT 仓库名称, address VARCHAR(255) COMMENT 仓库地址, manager VARCHAR(50) COMMENT 负责人, status TINYINT DEFAULT 1 COMMENT 状态1启用0停用, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除, create_by VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_by VARCHAR(50), update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_warehouse_code (warehouse_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT仓库表;3.3 入库单和入库明细表-- 入库单主表 CREATE TABLE stk_inbound ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, inbound_no VARCHAR(50) NOT NULL COMMENT 入库单号, inbound_type TINYINT COMMENT 入库类型1采购入库2生产入库3退货入库4盘点入库, supplier_id BIGINT COMMENT 供应商ID, warehouse_id BIGINT NOT NULL COMMENT 收货仓库ID, inbound_date DATE NOT NULL COMMENT 入库日期, status TINYINT DEFAULT 0 COMMENT 状态0草稿1已审核2已作废, remark VARCHAR(500) COMMENT 备注, deleted TINYINT DEFAULT 0, create_by VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_by VARCHAR(50), update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_inbound_no (inbound_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT入库单主表; -- 入库明细表 CREATE TABLE stk_inbound_item ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, inbound_id BIGINT NOT NULL COMMENT 入库单主表ID, product_id BIGINT NOT NULL COMMENT 商品ID, quantity DECIMAL(18,3) NOT NULL COMMENT 入库数量, price DECIMAL(18,2) DEFAULT 0 COMMENT 入库单价, amount DECIMAL(18,2) DEFAULT 0 COMMENT 金额, remark VARCHAR(200) COMMENT 明细备注, KEY idx_inbound_id (inbound_id), KEY idx_product_id (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT入库明细表;3.4 出库单和出库明细表出库表和入库表结构非常相似这里可以单独建表也可以设计成一张通用库存单据表。对于初学者来说分表更容易理解。如果业务扩展后单据类型增多再考虑将公共字段抽取为通用单据表。-- 出库单主表 CREATE TABLE stk_outbound ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, outbound_no VARCHAR(50) NOT NULL COMMENT 出库单号, outbound_type TINYINT COMMENT 出库类型1销售出库2生产领料3报损出库4盘亏出库, customer_id BIGINT COMMENT 客户ID, warehouse_id BIGINT NOT NULL COMMENT 发货仓库ID, outbound_date DATE NOT NULL COMMENT 出库日期, status TINYINT DEFAULT 0 COMMENT 状态0草稿1已审核2已作废, remark VARCHAR(500) COMMENT 备注, deleted TINYINT DEFAULT 0, create_by VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_by VARCHAR(50), update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_outbound_no (outbound_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT出库单主表; -- 出库明细表 CREATE TABLE stk_outbound_item ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, outbound_id BIGINT NOT NULL COMMENT 出库单主表ID, product_id BIGINT NOT NULL COMMENT 商品ID, quantity DECIMAL(18,3) NOT NULL COMMENT 出库数量, price DECIMAL(18,2) DEFAULT 0 COMMENT 出库单价, amount DECIMAL(18,2) DEFAULT 0 COMMENT 金额, remark VARCHAR(200) COMMENT 明细备注, KEY idx_outbound_id (outbound_id), KEY idx_product_id (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT出库明细表;3.5 库存流水表和库存余额表库存流水表和库存余额表是整个库存系统的核心。简单来说流水表记录了每一次库存变化余额表保存当前库存数量。查询报表时可以先看余额表要追溯时可以查流水表。-- 库存流水表 CREATE TABLE stk_stock_flow ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, flow_no VARCHAR(50) NOT NULL COMMENT 流水号, product_id BIGINT NOT NULL COMMENT 商品ID, warehouse_id BIGINT NOT NULL COMMENT 仓库ID, change_type TINYINT COMMENT 变动类型1入库2出库3盘点调整4调拨出5调拨入, source_no VARCHAR(50) COMMENT 来源单据号, source_type TINYINT COMMENT 来源单据类型1入库单2出库单3盘点单4调拨单, quantity_change DECIMAL(18,3) NOT NULL COMMENT 变动数量入库为正出库为负, before_quantity DECIMAL(18,3) DEFAULT 0 COMMENT 变动前库存, after_quantity DECIMAL(18,3) DEFAULT 0 COMMENT 变动后库存, create_by VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_flow_no (flow_no), KEY idx_product_warehouse (product_id, warehouse_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存流水表; -- 库存余额表 CREATE TABLE stk_stock_balance ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, product_id BIGINT NOT NULL COMMENT 商品ID, warehouse_id BIGINT NOT NULL COMMENT 仓库ID, stock_quantity DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT 库存数量, locked_quantity DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT 锁定数量, available_quantity DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT 可用数量, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_by VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_by VARCHAR(50), update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_product_warehouse (product_id, warehouse_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存余额表;这里引入了一个“锁定数量”字段。这个概念在电商库存系统中用得非常多。当用户提交订单但还没付款时可以先占用一部分库存避免其他订单继续超卖。在普通ERP库存系统中如果业务流程没有“预占”环节可以暂时不用这个字段但保留它能为以后的复杂业务留出扩展空间。3.6 为什么需要 before_quantity 和 after_quantity在设计库存流水表时有人会问既然有了 quantity_change为什么还要存 before_quantity 和 after_quantity原因很简单便于对账和排查。如果某一天发现库存余额不平可以通过流水表快速看到每笔操作发生前后的库存快照。特别是在多人同时操作的情况下光靠变动数量无法还原当时的库存上下文。保留这两个字段相当于给每次库存变动记录了系统日志。4. 后端核心实现Spring Boot 库存服务4.1 项目基础结构假设我们使用Spring Boot MyBatis-Plus来构建后端服务项目结构如下erp-stock-system ├── pom.xml └── src └── main ├── java │ └── com │ └── example │ └── erp │ ├── ErpApplication.java │ ├── common │ │ ├── Result.java │ │ └── BusinessException.java │ ├── controller │ │ └── StockController.java │ ├── entity │ │ ├── StockBalance.java │ │ ├── StockFlow.java │ │ ├── Inbound.java │ │ └── Outbound.java │ ├── mapper │ │ ├── StockBalanceMapper.java │ │ ├── StockFlowMapper.java │ │ ├── InboundMapper.java │ │ └── OutboundMapper.java │ └── service │ ├── InboundService.java │ └── OutboundService.java └── resources ├── application.yml └── mapper4.2 Maven 依赖配置dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies4.3 库存扣减的核心逻辑库存扣减是库存系统里最容易出Bug的地方。下面用一个出库场景来演示核心代码。注意这里的示例重点是事务和并发控制。首先定义库存余额实体Data TableName(stk_stock_balance) public class StockBalance { TableId(type IdType.AUTO) private Long id; private Long productId; private Long warehouseId; private BigDecimal stockQuantity; private BigDecimal lockedQuantity; private BigDecimal availableQuantity; Version private Integer version; }然后在Service中实现出库审核逻辑Service public class OutboundService { Resource private StockBalanceMapper stockBalanceMapper; Resource private StockFlowMapper stockFlowMapper; Transactional(rollbackFor Exception.class) public void auditOutbound(Outbound outbound, ListOutboundItem items) { for (OutboundItem item : items) { // 1. 查询库存余额 StockBalance balance stockBalanceMapper.selectByProductAndWarehouse( item.getProductId(), outbound.getWarehouseId()); if (balance null) { throw new BusinessException(库存余额不存在请先初始化库存); } // 2. 校验可用库存 if (balance.getAvailableQuantity().compareTo(item.getQuantity()) 0) { throw new BusinessException(商品库存不足); } // 3. 使用乐观锁更新库存 int rows stockBalanceMapper.deductStock( balance.getId(), item.getQuantity(), balance.getVersion()); if (rows 0) { throw new BusinessException(库存扣减失败请重试); } // 4. 写入库存流水 StockFlow flow new StockFlow(); flow.setFlowNo(generateFlowNo()); flow.setProductId(item.getProductId()); flow.setWarehouseId(outbound.getWarehouseId()); flow.setChangeType(2); flow.setSourceNo(outbound.getOutboundNo()); flow.setSourceType(2); flow.setQuantityChange(item.getQuantity().negate()); flow.setBeforeQuantity(balance.getStockQuantity()); flow.setAfterQuantity(balance.getStockQuantity().subtract(item.getQuantity())); flow.setCreateBy(outbound.getUpdateBy()); stockFlowMapper.insert(flow); } // 5. 更新出库单状态为已审核 outbound.setStatus(1); outboundMapper.updateById(outbound); } }对应的Mapper SQL如下update iddeductStock UPDATE stk_stock_balance SET stock_quantity stock_quantity - #{quantity}, available_quantity available_quantity - #{quantity}, version version 1 WHERE id #{id} AND version #{version} AND available_quantity gt; #{quantity} /update这段SQL的关键点在于UPDATE语句中同时带上了version条件并且通过available_quantity大于等于待扣数量来做额外的库存约束。在数据库层面这种写法可以防止并发场景下两个请求同时扣减同一份库存。4.4 入库审核的核心逻辑入库相对简单核心是生成流水并累加库存Transactional(rollbackFor Exception.class) public void auditInbound(Inbound inbound, ListInboundItem items) { for (InboundItem item : items) { StockBalance balance stockBalanceMapper.selectByProductAndWarehouse( item.getProductId(), inbound.getWarehouseId()); if (balance null) { // 初始化一条库存余额记录 balance new StockBalance(); balance.setProductId(item.getProductId()); balance.setWarehouseId(inbound.getWarehouseId()); balance.setStockQuantity(BigDecimal.ZERO); balance.setLockedQuantity(BigDecimal.ZERO); balance.setAvailableQuantity(BigDecimal.ZERO); stockBalanceMapper.insert(balance); } // 增加库存 int rows stockBalanceMapper.addStock( balance.getId(), item.getQuantity(), balance.getVersion()); if (rows 0) { throw new BusinessException(库存更新失败请重试); } // 写入流水 StockFlow flow new StockFlow(); flow.setFlowNo(generateFlowNo()); flow.setProductId(item.getProductId()); flow.setWarehouseId(inbound.getWarehouseId()); flow.setChangeType(1); flow.setSourceNo(inbound.getInboundNo()); flow.setSourceType(1); flow.setQuantityChange(item.getQuantity()); flow.setBeforeQuantity(balance.getStockQuantity()); flow.setAfterQuantity(balance.getStockQuantity().add(item.getQuantity())); flow.setCreateBy(inbound.getUpdateBy()); stockFlowMapper.insert(flow); } inbound.setStatus(1); inboundMapper.updateById(inbound); }这里有几个容易被忽略的点新增库存余额记录时要注意并发插入。如果在极短时间内同时收到两条入库请求且库存余额记录不存在可能会插入两条重复记录。解决方式有两种一是利用数据库唯一索引uk_product_warehouse兜底二是先查询如果不存在则通过INSERT IGNORE或ON DUPLICATE KEY UPDATE方式处理。流水号建议使用类似“年月日随机数”或“日期序列”的格式。这里可以使用Redis自增序列也可以直接用数据库的ID生成器但一定要保证唯一。4.5 事务与并发控制方案选择在库存扣减场景中常见的并发控制方案有三种方案实现方式优点缺点乐观锁使用version字段更新时校验版本号简单、性能好冲突时需要重试悲观锁SELECT ... FOR UPDATE锁定记录实现直观不会重试并发高时锁竞争激烈数据库约束使用条件更新 WHERE available_quantity quantity数据库层面兜底需要配合乐观锁或事务使用实际项目中我比较推荐“乐观锁 条件更新”的组合方式。先用乐观锁保证版本一致性再用数据库条件更新作为最终约束这样既不会因为锁等待导致系统卡顿又能防止超卖。5. 前端页面Vue 3 Element Plus 快速搭建5.1 前端项目结构前端部分使用Vue 3 Vite Element Plus项目结构如下erp-web ├── index.html ├── package.json ├── vite.config.js └── src ├── main.js ├── App.vue ├── api │ └── stock.js └── views ├── dashboard.vue ├── inbound.vue ├── outbound.vue └── stock.vue5.2 库存余额展示页面下面是一个简单的库存余额列表页面template div classstock-page el-card el-form :inlinetrue el-form-item label商品编码 el-input v-modelqueryParam.productCode placeholder请输入商品编码 clearable / /el-form-item el-form-item label仓库 el-select v-modelqueryParam.warehouseId placeholder请选择仓库 clearable el-option v-foritem in warehouseList :keyitem.id :labelitem.warehouseName :valueitem.id / /el-select /el-form-item el-form-item el-button typeprimary clickloadStockList查询/el-button el-button clickresetQuery重置/el-button /el-form-item /el-form /el-card el-card stylemargin-top: 16px el-table :datastockList border stripe el-table-column propproductCode label商品编码 min-width120 / el-table-column propproductName label商品名称 min-width150 / el-table-column propwarehouseName label仓库名称 min-width120 / el-table-column propstockQuantity label库存数量 min-width100 alignright / el-table-column proplockedQuantity label锁定数量 min-width100 alignright / el-table-column propavailableQuantity label可用数量 min-width100 alignright / /el-table /el-card /div /template script setup import { ref, onMounted } from vue import { getStockList } from /api/stock const queryParam ref({ productCode: , warehouseId: null }) const stockList ref([]) const warehouseList ref([]) async function loadStockList() { const res await getStockList(queryParam.value) stockList.value res.data.records } function resetQuery() { queryParam.value { productCode: , warehouseId: null } loadStockList() } onMounted(() { loadStockList() }) /script这个页面的作用是展示当前库存余额让管理者可以快速看到某个仓库中所有商品的数量分布。实际系统中还可以加入库存预警阈值当可用库存低于某个下限时在列表里用红色标识提示需要补货。5.3 前端接口封装import request from /utils/request export function getStockList(params) { return request({ url: /api/stock/list, method: get, params }) } export function auditInbound(data) { return request({ url: /api/inbound/audit, method: post, data }) } export function auditOutbound(data) { return request({ url: /api/outbound/audit, method: post, data }) }前端做好数据展示和交互即可真正的业务规则必须放在后端服务中。尤其是库存扣减、事务、权限校验这类逻辑如果放在前端任何人都可以通过浏览器控制台绕过。6. 开源许可证与二次开发注意事项6.1 许可证怎么选很多开发者在Gitee或GitHub上创建开源项目时都会纠结许可证怎么选。这里给出一个简单的选择建议许可证特点适合场景MIT宽松允许商用允许闭源个人项目、内部系统、希望被广泛使用Apache 2.0宽松附带专利授权条款企业级开源项目GPL传染性衍生代码需开源希望生态保持开源LGPL修改库文件才需要开源被其他项目以库形式引用MPL文件级别传染混合开源开发模型如果你在开发开源ERP库存管理系统需要明确一个问题允许别人拿去商用而不开源吗如果允许建议MIT或Apache 2.0如果希望商业公司在使用后把增强功能回馈社区可以选择GPL。6.2 二次开发时的注意事项保留上游项目版权声明不要删除License文件。修改代码时尽量以模块化方式扩展避免在核心代码上大面积改动。如果是基于GPL项目二次开发对外分发时需要注意源代码开放义务。如果只是企业内部使用不分发给第三方GPL和MIT的影响相对可控但也要咨询法务。7. 常见问题与排查思路7.1 常见问题汇总表问题现象常见原因解决思路库存扣减后变成负数未使用乐观锁或条件更新并发扣减在UPDATE语句中增加可用库存判断使用version同一商品出现多条库存余额记录并发插入库存余额唯一索引失效创建唯一索引插入时使用ON DUPLICATE KEY UPDATE页面显示库存和实际盘点不一致手工修改数据库导致流水断裂禁止手工改库存通过盘点单调整超卖订单提交和库存扣减不是同一事务将订单创建与库存扣减放在同一事务中流水号重复使用时间戳或简单随机数生成流水号使用Redis自增或数据库序列报表查询慢库存流水表全表扫描为product_id、warehouse_id、create_time建立联合索引Decimal类型计算出现精度问题使用double或float存储金额金额和数量统一使用DECIMAL7.2 复盘一个典型的库存变负事故假设系统上线后运营反馈某个商品库存从“3件”变成了“-1件”。排查思路如下查询该商品的库存流水表看看最近有哪些出入库记录。找出导致库存变负的那条流水确认来源单据。检查该单据审核时后端是否执行了“可用库存是否充足”的判断。如果判断逻辑存在再看是否是并发导致的两个请求同时读到库存为3同时扣2理论上最终结果应该是1但由于没有版本号控制两条SQL都成功执行结果变成-1。修复方式为库存余额表增加version字段修改UPDATE语句增加available_quantity quantity条件。这个案例说明库存系统不是“能增能减”就行必须把并发控制放到数据库层而不是依赖应用层的判断。8. 最佳实践与工程建议8.1 单据驱动禁止直接改库存很多初学ERP开发的朋友会犯一个错误用户点击“库存调整”直接写一条UPDATE语句把库存数量改了。这样做表面上很快实际是在给系统埋雷。正确的做法是引入“盘点单”。盘点的本质是“账面数量 vs 实盘数量”差异部分通过盘点单生成盘盈或盘亏流水最终调整库存余额。这样做的好处是所有库存变动都可追溯每笔变更都能找到对应的业务单据。8.2 库存流水只增不改库存流水表记录了企业所有库存变动的历史是一份极其重要的审计数据。设计上应该做到流水表不提供修改和删除接口。即使对应的入库单或出库单作废流水表仍然保留。作废单据时需要生成一张反向冲销的流水而不是物理删除原来的流水。这样设计虽然会增加一些开发量但长期来看系统的数据可信度会高很多。8.3 永远在事务中操作库存所有涉及库存余额变化的操作都必须放在数据库事务中。Spring中可以通过Transactional注解实现。事务的隔离级别建议设置为默认的数据库隔离级别不要轻易提高。另外事务中尽量缩短业务逻辑的执行时间不要在事务内调用第三方HTTP接口、发送短信邮件、等待MQ消息等耗时操作。否则会导致数据库连接长时间占用系统吞吐量下降。8.4 权限与安全边界库存管理系统涉及企业的核心资产数据安全设计不能放松接口必须做登录认证不能裸奔。权限控制建议做到按钮级别不只是页面级别。审核入库单和创建入库单建议由不同角色承担形成简单复核机制。所有写操作都要记录操作日志包括操作人、操作时间、IP、操作内容。不要在前端页面暴露全量库存导出接口或者至少加入权限校验和数据量限制防止数据被批量拉取。8.5 数据备份与灰度发布生产环境数据库必须配置自动备份策略。库存数据一旦丢失或错乱恢复成本极高。建议每周至少做一次全量备份每天做一次增量备份备份文件要存储到独立存储空间。系统升级时先在测试环境完整跑一遍出入库流程确认无问题后再发布到生产。如果条件允许可以采用灰度发布方式先让仓库部门的少量账号使用新版本验证稳定后再全员切换。8.6 性能优化要点库存流水表数据量会随着业务增长快速膨胀建议按季度或按年做分区。常用查询条件要建立联合索引例如(product_id, warehouse_id, create_time)。库存余额表的查询非常频繁尽量通过组合条件唯一命中记录避免全表扫描。报表类查询不要直接在业务库执行可以通过定时任务把数据同步到统计库或者使用读从库。9. 总结与学习路线写到这里开源ERP库存管理系统的核心内容已经拆解得比较完整了。如果你是从零开始学习ERP开发建议按下面的路线继续深入先理解“单据 流水 余额”三层模型这是整个库存系统的灵魂。自己动手建一套MySQL表结构手工插入几条入库单、出库单数据查看库存余额变化。学习Spring Boot事务管理和MyBatis-Plus的乐观锁插件写一个简单的库存扣减接口。完善单据审核流程、库存盘点、库存预警、调拨单逐步扩展系统能力。阅读优秀开源项目的源码比如Odoo库存模块、ERPNext的Stock模块学习它们的业务抽象方式。最后尝试把库存模块与采购、销售、财务模块打通理解ERP系统各模块之间的关联。开源ERP库存管理系统的优势在于你可以边看源码边学习遇到不满足的业务需求还能自己改。但这也意味着你需要投入时间和精力不能把自己当成普通的软件使用者。如果你所在的企业正好需要一个库存管理系统不妨先从本文提到的数据模型和核心接口入手搭一个最小可运行版本再根据实际业务去迭代。我在文章里分享的SQL和Java代码均是可以直接参考的核心片段。实际项目里你还需要补充用户认证、菜单权限、操作日志、报表统计等功能。如果遇到库存扣减并发问题优先检查你们的库存余额表是否使用了乐观锁以及UPDATE语句是否带有库存数量约束条件——大部分问题都出在这两个地方。如果你准备把某个开源ERP项目引入公司也不要急着部署。先让运维和开发团队把技术栈、部署方式、License条款、二次开发成本都评估清楚。库存系统是业务系统的核心一旦跑了错误的数据业务方对系统的信任度会大打折扣。希望这篇文章能帮你少走一些弯路。
返回列表