本文目录导读:

实现一个PHP项目的货位管理(Warehouse Location Management),核心是 “库位编码” 与 “库存绑定”,就是记录“哪个商品、在哪个货架、哪个层、哪个格子”。
以下是一个从数据库设计到代码实现的完整方案,适用于进销存(WMS)或电商后台系统。
第一阶段:数据库设计
货位管理的核心是三张表:仓库表、货位表、库存表。
-- 1. 仓库表 CREATE TABLE `warehouse` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '仓库名称', `code` varchar(20) NOT NULL COMMENT '仓库编码', `address` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `code` (`code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 2. 货位表 (核心表) CREATE TABLE `location` ( `id` int(11) NOT NULL AUTO_INCREMENT, `warehouse_id` int(11) NOT NULL COMMENT '所属仓库', `code` varchar(50) NOT NULL COMMENT '货位编码,如 A-01-01-01', `area` varchar(50) DEFAULT NULL COMMENT '分区(如 A区、B区、收货区)', `shelf` varchar(50) DEFAULT NULL COMMENT '货架号', `layer` varchar(20) DEFAULT NULL COMMENT '层', `slot` varchar(20) DEFAULT NULL COMMENT '格子', `status` tinyint(1) DEFAULT '0' COMMENT '状态:0空闲,1占用,2锁定,3损坏', `capacity` decimal(10,2) DEFAULT NULL COMMENT '最大容量(体积或重量)', `created_at` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uniq_code` (`warehouse_id`, `code`), -- 同一仓库下货位编码唯一 KEY `warehouse_id` (`warehouse_id`), FOREIGN KEY (`warehouse_id`) REFERENCES `warehouse` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 3. 库存表 (关联货位) CREATE TABLE `inventory` ( `id` int(11) NOT NULL AUTO_INCREMENT, `product_id` int(11) NOT NULL COMMENT '商品ID', `location_id` int(11) NOT NULL COMMENT '货位ID', `batch_no` varchar(50) DEFAULT NULL COMMENT '批次号', `quantity` int(11) NOT NULL DEFAULT '0' COMMENT '数量', `locked_quantity` int(11) DEFAULT '0' COMMENT '锁定数量(预占)', `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uniq_product_location_batch` (`product_id`, `location_id`, `batch_no`), KEY `location_id` (`location_id`), FOREIGN KEY (`location_id`) REFERENCES `location` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
第二阶段:货位编码规范
推荐使用层级编码,便于系统自动解析和人员现场识别。
格式: 仓库码-分区-货架-层-格子
示例: WH01-A-03-02-01
- WH01:仓库
- A:A区
- 03:3号货架
- 02:第2层
- 01:第1个格子
PHP生成方法:
function generateLocationCode(string $warehouseCode, string $area, string $shelf, string $layer, string $slot): string {
return strtoupper("{$warehouseCode}-{$area}-{$shelf}-{$layer}-{$slot}");
}
// 调用:generateLocationCode('WH01', 'A', '03', '02', '01')
// 结果:WH01-A-03-02-01
第三阶段:核心业务逻辑(PHP实现)
初始化货位(批量创建)
class LocationService {
public function createLocations(array $data): array {
$db = getDB(); // 假设有数据库连接
$result = ['success' => 0, 'errors' => []];
foreach ($data as $loc) {
$code = generateLocationCode(
$loc['warehouse_code'],
$loc['area'],
$loc['shelf'],
$loc['layer'],
$loc['slot']
);
// 检查唯一性
$stmt = $db->prepare("SELECT id FROM location WHERE warehouse_id = ? AND code = ?");
$stmt->execute([$loc['warehouse_id'], $code]);
if ($stmt->fetch()) {
$result['errors'][] = "货位 {$code} 已存在";
continue;
}
// 插入
$insert = $db->prepare(
"INSERT INTO location (warehouse_id, code, area, shelf, layer, slot, status)
VALUES (?, ?, ?, ?, ?, ?, 0)"
);
$insert->execute([
$loc['warehouse_id'], $code, $loc['area'],
$loc['shelf'], $loc['layer'], $loc['slot']
]);
$result['success']++;
}
return $result;
}
}
商品入库(上架到指定货位)
关键点:更新库存表 + 更新货位状态(需用事务)。
class InventoryService {
public function putaway(int $productId, int $locationId, int $quantity, ?string $batchNo) {
$db = getDB();
$db->beginTransaction();
try {
// 1. 检查货位状态
$locStmt = $db->prepare("SELECT status FROM location WHERE id = ? FOR UPDATE");
$locStmt->execute([$locationId]);
$loc = $locStmt->fetch();
if ($loc['status'] != 0) {
throw new \Exception("货位不可用");
}
// 2. 检查是否有库存记录(同一商品+同一货位+同一批次)
$invStmt = $db->prepare(
"SELECT id, quantity FROM inventory
WHERE product_id = ? AND location_id = ? AND (batch_no = ? OR (batch_no IS NULL AND ? IS NULL))
FOR UPDATE"
);
$invStmt->execute([$productId, $locationId, $batchNo, $batchNo]);
$inventory = $invStmt->fetch();
if ($inventory) {
// 已有记录,增加数量
$update = $db->prepare("UPDATE inventory SET quantity = quantity + ? WHERE id = ?");
$update->execute([$quantity, $inventory['id']]);
} else {
// 新记录
$insert = $db->prepare(
"INSERT INTO inventory (product_id, location_id, batch_no, quantity) VALUES (?, ?, ?, ?)"
);
$insert->execute([$productId, $locationId, $batchNo, $quantity]);
}
// 3. 标记货位为占用
$updateLoc = $db->prepare("UPDATE location SET status = 1 WHERE id = ?");
$updateLoc->execute([$locationId]);
$db->commit();
return true;
} catch (\Exception $e) {
$db->rollBack();
throw $e;
}
}
}
查询库存所在货位
class ProductService {
public function getLocationByProduct(int $productId): array {
$db = getDB();
$stmt = $db->prepare(
"SELECT l.code as location_code, l.area, l.shelf, l.layer, l.slot,
i.quantity, i.batch_no
FROM inventory i
JOIN location l ON i.location_id = l.id
WHERE i.product_id = ?
ORDER BY l.code"
);
$stmt->execute([$productId]);
return $stmt->fetchAll();
}
}
货位推荐算法(入库时自动推荐)
class LocationRecommendService {
public function recommendLocation(int $warehouseId, int $productId): ?int {
// 策略:优先推荐同一商品已有库存的货位(合并存放)
// 其次推荐空闲货位(按货位编码排序,从A区开始)
$db = getDB();
// 1. 查找同一商品已经占用的货位
$stmt = $db->prepare(
"SELECT l.id, l.code
FROM inventory i
JOIN location l ON i.location_id = l.id
WHERE l.warehouse_id = ? AND i.product_id = ?
ORDER BY l.code
LIMIT 1"
);
$stmt->execute([$warehouseId, $productId]);
$existing = $stmt->fetch();
if ($existing) {
return $existing['id']; // 合并存放
}
// 2. 如果没有,找空闲货位(按编码排序,从A01开始)
$stmt = $db->prepare(
"SELECT id, code FROM location
WHERE warehouse_id = ? AND status = 0
ORDER BY code
LIMIT 1"
);
$stmt->execute([$warehouseId]);
$free = $stmt->fetch();
return $free ? $free['id'] : null; // null表示仓库已满
}
}
货位可视化(简单列表展示)
// 控制器
public function index(Request $request) {
$warehouseId = $request->get('warehouse_id', 1);
$locations = $this->locationService->getLocationsTree($warehouseId);
echo $this->render('locations/index', ['locations' => $locations]);
}
// Service
public function getLocationsTree(int $warehouseId): array {
$db = getDB();
$stmt = $db->prepare(
"SELECT l.*, i.product_id, i.quantity
FROM location l
LEFT JOIN inventory i ON l.id = i.location_id
WHERE l.warehouse_id = ?
ORDER BY l.area, l.shelf, l.layer, l.slot"
);
$stmt->execute([$warehouseId]);
$rows = $stmt->fetchAll();
// 整理成树形结构,便于前端展示
$tree = [];
foreach ($rows as $row) {
$tree[$row['area']][$row['shelf']][$row['layer']][] = $row;
}
return $tree;
}
第四阶段:前端交互与辅助功能
货位批量录入
- 方式:后台提供一个可视化表格,管理员输入
起始货架、层数、每层格子数,系统自动生成编码。function batchGenerate(int $warehouseId, string $area, string $startShelf, int $shelfCount, int $layers, int $slots): array { $data = []; for ($s = 0; $s < $shelfCount; $s++) { $shelf = str_pad($startShelf + $s, 2, '0', STR_PAD_LEFT); for ($l = 1; $l <= $layers; $l++) { for ($sl = 1; $sl <= $slots; $sl++) { $data[] = [ 'warehouse_id' => $warehouseId, 'area' => $area, 'shelf' => $shelf, 'layer' => str_pad($l, 2, '0', STR_PAD_LEFT), 'slot' => str_pad($sl, 2, '0', STR_PAD_LEFT), ]; } } } return $data; }
货位扫码
- 原理:每个货位生成二维码(内容为
code字段),PDA扫码后,PHP接口接收扫码结果,直接定位货位。// 扫码查询接口 public function scan(string $locationCode): array { $db = getDB(); $stmt = $db->prepare( "SELECT l.*, GROUP_CONCAT(CONCAT(p.name, ' x', i.quantity) SEPARATOR ', ') as products FROM location l LEFT JOIN inventory i ON l.id = i.location_id LEFT JOIN products p ON i.product_id = p.id WHERE l.code = ? GROUP BY l.id" ); $stmt->execute([$locationCode]); return $stmt->fetch() ?: ['error' => '货位不存在']; }
库存移动/调拨
public function moveStock(int $fromLocationId, int $toLocationId, int $productId, int $quantity) {
$db = getDB();
$db->beginTransaction();
try {
// 1. 扣减源货位
$stmt = $db->prepare("UPDATE inventory SET quantity = quantity - ? WHERE location_id = ? AND product_id = ?");
$stmt->execute([$quantity, $fromLocationId, $productId]);
// 若源货位数量变为0,释放货位
$check = $db->prepare("SELECT SUM(quantity) as total FROM inventory WHERE location_id = ?");
$check->execute([$fromLocationId]);
if ($check->fetch()['total'] == 0) {
$db->prepare("UPDATE location SET status = 0 WHERE id = ?")->execute([$fromLocationId]);
}
// 2. 增加目标货位(逻辑同入库)
// ... (参考上文的 putaway 子逻辑)
$db->commit();
} catch (\Exception $e) {
$db->rollBack();
throw $e;
}
}
PHP货位管理的核心要点
- 编码统一:
仓库-区-架-层-位,是系统与物理世界的映射。 - 事务必须:库存变动(入库、出库、移动)务必使用 数据库事务,防止数据不一致。
- 状态分离:
location.status只表示货位物理状态(空闲/占用/损坏),库存数量由inventory.quantity管理。 - 锁定机制:高并发场景下(如PDA抢单),使用
SELECT ... FOR UPDATE行锁。 - 扩展性:未来如果增加RFID、AGV调度,只需在
location表增加rfid_tag、coordinate_x/y等字段即可。
方案基于纯PHP + MySQL,没有依赖特定框架,你可以直接适配到 Laravel、ThinkPHP、Yii 等框架中。