很多食堂采购系统做不稳定,不是界面问题,而是底层数据结构和库存算法没设计好。
常见翻车现场你一定见过:
- 库存经常对不上
- 入库出库顺序混乱
- 成本算不准
- 多食堂同时扣库存直接变负数
- 月底对账全靠人工补Excel
说白了:数据库结构不规范 + 库存算法太粗糙。
真正可商用的食堂采购系统源码,核心就两件事:
第一,表结构要可追溯
第二,库存算法要强一致
下面我用一套可直接落地的设计方案,把关键实现从表结构到代码完整拆开讲清楚。
一、核心业务流程梳理
先统一一个标准流程:
采购申请 → 采购单 → 入库 → 库存累加
领料/消耗 → 出库 → 库存扣减
盘点 → 差异调整
月底 → 成本核算 + 对账
所以数据库至少要支撑:
- 供应商管理
- 商品管理
- 仓库管理
- 采购入库
- 出库领料
- 实时库存
- 库存流水
记住一句话:
库存 = 汇总结果
流水 = 真正依据
库存表只是“缓存”,库存流水才是“真相”。
二、核心数据库表结构设计
技术栈示例:
SpringBoot + MySQL + MyBatis
1 商品表 goods
CREATE TABLE goods (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
category_id BIGINT,
unit VARCHAR(20),
spec VARCHAR(100),
enabled TINYINT DEFAULT 1,
created_at DATETIME
);
作用:基础物料信息
示例:
- 大米 50kg/袋
- 鸡蛋 30枚/箱
2 供应商表 supplier
CREATE TABLE supplier (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(200),
contact VARCHAR(50),
phone VARCHAR(20),
status TINYINT DEFAULT 1
);
3 仓库表 warehouse
CREATE TABLE warehouse (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
location VARCHAR(200)
);
支持:
- 主仓
- 冷藏仓
- 分校区仓库
4 库存表 inventory(实时库存)
高频查询表
CREATE TABLE inventory (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
goods_id BIGINT,
warehouse_id BIGINT,
quantity DECIMAL(10,2) DEFAULT 0,
amount DECIMAL(12,2) DEFAULT 0,
version INT DEFAULT 0,
UNIQUE KEY uk_goods_wh(goods_id, warehouse_id)
);
关键字段:
- quantity 当前数量
- amount 总成本
- version 乐观锁
5 库存流水表 inventory_log(核心)
这是最重要的一张表
CREATE TABLE inventory_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
goods_id BIGINT,
warehouse_id BIGINT,
type VARCHAR(20),
quantity DECIMAL(10,2),
price DECIMAL(10,2),
amount DECIMAL(12,2),
ref_no VARCHAR(50),
created_at DATETIME
);
type:
- IN 入库
- OUT 出库
- ADJUST 盘点调整
所有库存变化必须写这张表。
三、库存算法设计思路
很多人直接:
update inventory set quantity = quantity - 10
这种做法必出事故。
正确思路:
库存变更三步走:
① 写库存流水
② 扣减库存(带锁)
③ 校验结果
四、入库算法实现(加库存)
Service 实现
@Transactional
public void stockIn(Long goodsId, Long warehouseId,
BigDecimal qty, BigDecimal price) {
BigDecimal amount = qty.multiply(price);
// 1 写流水
inventoryLogMapper.insert(new InventoryLog(
goodsId, warehouseId, "IN", qty, price, amount
));
// 2 更新库存
inventoryMapper.addStock(goodsId, warehouseId, qty, amount);
}
Mapper SQL
UPDATE inventory
SET quantity = quantity + #{
qty},
amount = amount + #{
amount},
version = version + 1
WHERE goods_id = #{
goodsId}
AND warehouse_id = #{
warehouseId};
五、出库算法实现(防止超卖)
重点来了。
出库一定要防:
- 并发扣减
- 库存负数
推荐方案:
乐观锁 + 条件扣减
核心SQL
UPDATE inventory
SET quantity = quantity - #{
qty},
amount = amount - #{
amount},
version = version + 1
WHERE goods_id = #{
goodsId}
AND warehouse_id = #{
warehouseId}
AND quantity >= #{
qty}
AND version = #{
version};
如果影响行数为 0 → 扣减失败。
Java实现
@Transactional
public void stockOut(Long goodsId, Long warehouseId,
BigDecimal qty) {
Inventory inv = inventoryMapper.select(goodsId, warehouseId);
if (inv.getQuantity().compareTo(qty) < 0) {
throw new RuntimeException("库存不足");
}
BigDecimal avgPrice =
inv.getAmount().divide(inv.getQuantity(), 2, RoundingMode.HALF_UP);
BigDecimal amount = avgPrice.multiply(qty);
int rows = inventoryMapper.reduceStock(
goodsId, warehouseId, qty, amount, inv.getVersion());
if (rows == 0) {
throw new RuntimeException("库存并发冲突,请重试");
}
inventoryLogMapper.insert(
new InventoryLog(goodsId, warehouseId, "OUT", qty, avgPrice, amount)
);
}
六、成本算法(加权平均法)
食堂场景推荐:
加权平均法
原因:
- 计算简单
- 实时成本准确
- 不用复杂批次管理
公式:
新平均价 = (旧金额 + 入库金额) ÷ (旧数量 + 入库数量)
SQL 示例:
amount / quantity
直接算即可。
七、高并发优化建议
如果是多校区或集团食堂,订单并发高时:
必须加:
1 分库分表(按仓库拆)
2 Redis库存缓存
3 批量入库写入
4 异步流水日志
否则库存表会成为瓶颈。
八、总结一句实战经验
如果你正在做食堂采购系统源码,记住这三条铁律:
库存只查 inventory
对账只查 inventory_log
任何库存变化必须走事务
这样系统跑几年都不会乱。
真正商用级系统,拼的不是功能多,而是:
数据稳定 + 算法可靠 + 并发安全
底层打牢,上层再怎么扩展都不怕。