好吧,各位,把你们的笔记本摊开,咖啡端起来,甚至可以把手机扔一边——今天我们不聊那些花里胡哨的框架,也不扯那些听着让人头晕的微服务架构。我们来聊点硬核的,聊点“社畜”和“仓库大叔”都喜欢的救命稻草。
今天我们要探讨的主题是:如何用 PHP 这种“老古董”搭配 n8n 这种“自动化神器”,构建一条基于 Webhook 的工业级物料管理与自动预警链路。
想象一下,你是一个仓库主管。每天早上醒来,第一件事不是看股市,而是打开 Excel,心里默念:“千万别崩,千万别崩。” 然后你开始核对进货单、出货单、库存积压……这时候,如果有个不睡觉的机器人帮你盯着,告诉你:“嘿,螺丝A库存低于10个了,老板正在看报表,你打算怎么办?” 是不是听起来像天堂?
这玩意儿就能实现。而且,它不需要你写多少 JavaScript,也不需要你重构整个公司系统。我们就是要在 PHP 的稳定内核和 n8n 的灵活神经之间,搭起一座鹊桥。
准备好了吗?让我们开始这场关于“自动化”的巡回讲座。
第一部分:为什么要搞这套组合拳?
很多人看到 PHP 就头疼,看到 n8n 就两眼一抹黑。但咱们换个角度想。PHP 是谁?它是互联网的基石,是那些十年前写下的老项目,是只要有一台服务器就能跑起来的“战壕里的老兵”。它可靠、皮实、甚至有点丑,但它干活不挑食。
n8n 是谁?它是无代码/低代码自动化领域的摇滚明星。它就像个多才多艺的调酒师,能把数据从这头倒到那头,还能加点糖(逻辑)、加点冰(格式化)。
我们的策略是:让 PHP 擦地板(处理数据库、业务逻辑、业务闭环),让 n8n 去看门(接收信号、做判断、发消息)。
你想想,如果你把所有逻辑都塞进 PHP 里,当你想加一个“库存预警”功能时,你得改 PHP 代码、重启服务、测试环境、部署生产环境……这个过程快赶上生孩子了。但如果你用 n8n?你只需要拖一个 Webhook 节点进来,再拖一个逻辑判断节点,最后拖一个邮件节点,啪,搞定。不用重启服务器,甚至不需要写代码。
这就是 解耦 的艺术,也是工业级开发的精髓。我们这里要构建的,就是一个松耦合、高可用的全栈链路。
第二部分:蓝图设计——管道里的数据流
在敲代码之前,我们要先在脑海里画个图。这就像装修房子要先画图纸一样。
我们的数据流是这样的:
- 触发源:可能是 ERP 系统的接口,可能是某个扫码枪,甚至是你手动敲的一个 curl 命令。
- PHP 网关(服务员):接收请求,验证身份,更新数据库。这是核心业务,不能丢。
- Webhook 转发(传送带):PHP 更新完库存后,顺手给 n8n 发个 POST 请求,告诉它:“嘿,库存变了,你来过目一下。”
- n8n 工作流(安检员):收到数据,开始疯狂计算。如果是入库,没事走人;如果是出库,检查库存数。如果发现库存 < 阈值,这就不行了!
- 执行动作(发令枪):如果触发预警,n8n 就负责把消息推送到钉钉、企业微信或者发一封 5 分钟不读就撤回的邮件。
- 闭环:n8n 完事,任务结束。
在这个链路里,PHP 专注于“做正确的事”(维护数据一致性),n8n 专注于“把事做正确”(监控和通知)。
第三部分:PHP 端——老当益壮的基石
好了,图纸画完了,咱们开始砌砖。首先,我们得有个 PHP 接口来干活。
首先,我们得有个数据库表。别跟我提 MongoDB,咱们的物料管理还是 MySQL 这种关系型数据库比较稳妥。咱们设计个简单的 materials 表:
CREATE TABLE materials (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(50) NOT NULL COMMENT '物料编码',
name VARCHAR(100) NOT NULL COMMENT '物料名称',
quantity INT NOT NULL DEFAULT 0 COMMENT '当前库存',
min_stock INT NOT NULL DEFAULT 100 COMMENT '最低库存预警线',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这表结构很朴素,对吧?好,接下来是 PHP 的 API。我们这里用一种稍微现代一点的风格,不用那些臃肿的框架,就用原生 PHP 加上 PDO,轻量、敏捷、不容易出错。
我们的目标接口是 POST /api/update-stock。
<?php
// stock_handler.php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *'); // 开发环境爽一下,生产环境记得限制
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
// 1. 数据库连接 (别再硬编码密码了,环境变量了解一下?)
$dsn = 'mysql:host=localhost;dbname=factory_db;charset=utf8mb4';
$user = 'root';
$pass = 'password';
try {
$pdo = new PDO($dsn, $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Database connection failed']);
exit;
}
// 2. 获取并验证 JSON 数据
$input = json_decode(file_get_contents('php://input'), true);
if (!isset($input['material_code']) || !isset($input['change']) || !isset($input['type'])) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Missing required fields: material_code, change, type']);
exit;
}
$code = $input['material_code'];
$change = (int)$input['change'];
$type = $input['type']; // 'in' 或 'out'
// 3. 更新库存逻辑
// 注意:这里只是简化版,实际生产环境必须加事务
try {
$pdo->beginTransaction();
// 更新数量
$sql = "UPDATE materials SET quantity = quantity + :change WHERE code = :code";
$stmt = $pdo->prepare($sql);
$stmt->execute(['change' => $change, 'code' => $code]);
// 确保物料存在
if ($stmt->rowCount() === 0) {
throw new Exception("Material not found: " . $code);
}
// 获取更新后的库存信息,准备发给 n8n
$selectSql = "SELECT id, name, quantity, min_stock FROM materials WHERE code = :code";
$stmt = $pdo->prepare($selectSql);
$stmt->execute(['code' => $code]);
$material = $stmt->fetch(PDO::FETCH_ASSOC);
if ($material) {
// 如果是出库,并且库存低于预警线,标记一下,方便后面处理
$is_alert = ($type === 'out' && $material['quantity'] <= $material['min_stock']);
// 提交事务
$pdo->commit();
// 返回成功
http_response_code(200);
echo json_encode([
'status' => 'success',
'material' => $material,
'is_alert' => $is_alert
]);
} else {
$pdo->rollBack();
throw new Exception("Material not found");
}
} catch (Exception $e) {
$pdo->rollBack();
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
?>
看,这个 PHP 脚本是不是很眼熟?它做了所有繁琐的事情:连接数据库、解析 JSON、处理事务、返回结果。
但是,你发现了吗?这个 PHP 脚本虽然干完了活,但它不知道 n8n 是个什么东西,也不知道什么叫“预警”。它只知道:“我更新了数据,数据在 is_alert 字段里,你要不要用?”
这就像你把饭做好了,端上桌子,但是没喊客人吃。接下来,我们要请出 n8n 来当那个喊客人吃饭的角色。
第四部分:n8n 端——自动化流的炼金术
现在,打开你的 n8n 界面(默认是 http://localhost:5678)。你会看到一个白板,上面长满了花花绿绿的节点。
我们的任务是在上面画一条龙。具体步骤如下:
第一步:接收 Webhook
拖一个 Webhook 节点上来。双击它,设置:
- Method: POST
- Path:
/webhook/inventory-update - Response: JSON
这个节点就像是仓库门口的保安,只要有人来敲门(POST 请求),它就开门放行,然后把敲门人的信息(JSON 数据)吐给下一个节点。
第二步:处理数据
拖一个 Set 节点(或者 Function 节点)。
如果 PHP 返回的数据结构很简单,Set 节点就够用了。我们需要把 PHP 传过来的数据存到 n8n 的全局上下文里,或者直接往下传。
假设我们接收到了 JSON:
{
"status": "success",
"material": {
"name": "M6 螺丝",
"quantity": 5,
"min_stock": 10
},
"is_alert": true
}
我们在 Set 节点里,把 material.quantity 和 material.min_stock 抽取出来,准备做比较。
第三步:核心逻辑——Switch 节点
这是整个自动化流的灵魂。拖一个 Switch 节点。
在 Switch 的设置里,添加一个条件。我们根据 PHP 传来的 is_alert 字段来判断。
- Output 1: 对应
is_alert为true的情况。 - Output 2: 对应
is_alert为false的情况。
如果你不想要这个 is_alert,你也可以在 Switch 里直接比大小:{{ $json.material.quantity }} < {{ $json.material.min_stock }}。
第四步:执行预警动作
现在,让我们关注 Output 1(警报分支)。
这里就是我们真正干活的地方。
- 钉钉/企业微信 Webhook:拖一个 Webhook (Send Request) 节点。选择你的机器人 URL。
- 这里我们需要用 n8n 的 Code 节点或者 Set 节点构建一个漂亮的 JSON。
- 比如:
{ "msgtype": "markdown", "markdown": { "title": "🚨 库存告警!", "text": "### ⚠️ 警报n**物料名称**: {{ $json.material.name }}n**当前库存**: {{ $json.material.quantity }}n**预警线**: {{ $json.material.min_stock }}n**时间**: {{ $now }}" } }
- 发送邮件:拖一个 Send Email 节点。配置好 SMTP(Gmail、Outlook 或者你公司的邮件服务器)。输入收件人:
[email protected]。- 邮件标题:
【紧急】物料库存不足 - {{ $json.material.name }} - 邮件正文:同上。
- 邮件标题:
第五步:Else 分支
关注 Output 2(正常分支)。
这里可以什么都不做,或者记录一条日志说“库存正常,无操作”。
第五部分:串联两家——PHP 调用 n8n
现在,我们的 PHP 端已经更新了数据库,n8n 端已经搭建好了接收和处理逻辑。但是它们怎么说话呢?
我们需要在 PHP 的代码里加一小段逻辑:当库存更新成功且触发预警时,调用 n8n 的 Webhook。
我们在 stock_handler.php 的成功返回之前,加一个判断:
// ... (上面的代码省略) ...
if ($material) {
$is_alert = ($type === 'out' && $material['quantity'] <= $material['min_stock']);
$pdo->commit();
// === 新增部分:联动 n8n ===
if ($is_alert) {
$n8nWebhookUrl = 'http://your-n8n-instance.com/webhook/inventory-update';
$payload = [
'material_code' => $code,
'material_name' => $material['name'],
'quantity' => $material['quantity'],
'min_stock' => $material['min_stock']
];
// 使用 cURL 发送请求给 n8n
$ch = curl_init($n8nWebhookUrl);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// 记录一下日志,防止 n8n 挂了漏掉消息
error_log("[n8n Integration] Sent alert to n8n. HTTP Code: " . $httpCode);
// 注意:这里我们返回了 JSON,n8n 是能读到的
// 为了让 n8n 知道这是 alert,我们可能需要在 Payload 里加个 is_alert: true
// 或者,我们修改一下 PHP 返回的结构,把 is_alert 放到返回的 JSON 里。
// 上面的代码是合并了,所以可以直接返回。
}
// =========================
http_response_code(200);
echo json_encode([
'status' => 'success',
'material' => $material,
'is_alert' => $is_alert
]);
}
这样一来,整个闭环就通了:
- 老板在 ERP 里录入了一张出库单,数量 50 个,物料是螺丝。
- PHP 接收到请求,执行 SQL:
quantity - 50。 - 假设库存原本是 8 个,现在变成了 -42(或者我们逻辑是
8 - 50被限制为 0,视业务而定,假设是直接减少到 0)。 - PHP 发现
0 <= min_stock,设置$is_alert = true。 - PHP 生成 JSON,包含
is_alert: true,同时用 cURL 调用 n8n 的 URL。 - n8n 接收到请求,Webhook 节点触发。
- n8n 的 Switch 节点检测到
is_alert为 true,流量进入 Output 1。 - n8n 的 Set 节点处理数据,Webhook 节点发送钉钉消息:“老板,螺丝没了!”
- 老板收到消息,此时 PHP 还在等待 n8n 的响应(虽然我们代码里没等,但理想情况是等待)。
第六部分:工业级的“坑”与“补丁”
讲了这么多美好的画面,作为资深专家,我必须得泼点冷水。如果不解决下面这些问题,你的自动化流在大规模生产环境中会死得很惨。
1. 并发与重复预警
这是最大的坑。假设库存是 10,预警线是 10。
- 时间 T1:PHP A 节点更新库存为 9,发送请求给 n8n。
- 时间 T2:PHP B 节点更新库存为 8,发送请求给 n8n。
- 结果:n8n 收到两次请求,钉钉被刷屏了 20 条消息。老板会把你开除的。
解决方案(在 n8n 端):
在 n8n 的 Webhook 节点之前,加一个 Function 节点,或者在 Switch 节点之前做判断。
在 Function 节点里写逻辑:
// n8n Function Node Code
const data = $input.item.json;
// 这里我们要做一个简单的“去重”或者“聚合”
// 假设我们定义一个 key,比如物料编码
const materialCode = data.material_code;
// 我们可以检查 $env (环境变量) 或者使用 n8n 的内存存储(简单版)
// 这里为了演示,我们直接返回,但在实际生产中,你需要维护一个“最后发送预警时间”的列表
// 或者使用 n8n 的 Cron 节点进行定时汇总报警,而不是实时报警。
// 简单的防抖动逻辑示例
const lastAlertTime = $env.get(`alert_${materialCode}`) || 0;
const currentTime = new Date().getTime();
const timeDiff = currentTime - lastAlertTime;
if (timeDiff < 3600000) { // 如果距离上次报警不到1小时
return []; // 返回空数组,跳过本次报警
} else {
// 更新时间戳
$env.set(`alert_${materialCode}`, currentTime);
return $input.all(); // 放行
}
这招叫“冷却期”。别为了省事就实时报警,你半夜会被吵醒的。
2. 数据一致性
PHP 更新了数据库,但是 cURL 调用 n8n 失败了怎么办?
PHP 那边认为成功了(数据库已经提交),但老板没收到报警。
解决方案:
在 PHP 的错误处理里,如果是 $httpCode 不是 200,要把这次更新记录到一个“失败队列”表里,或者写进日志。然后你可以写一个 PHP 的定时任务,每天晚上 9 点去检查日志,给 n8n 发送一条“重试”指令。
3. 网络超时
n8n 是个独立的进程,有时候重启了,或者负载高了,响应就慢。
PHP 的 curl_exec 默认超时是 30 秒。如果 n8n 没反应,PHP 就会报错,前面的数据库事务还得回滚?不,我们上面没 rollback,那就尴尬了,数据丢了,报警也没发。
代码改进:
curl_setopt($ch, CURLOPT_TIMEOUT, 3); // 缩短超时到 3 秒
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 2); // 连接超时 2 秒
// 这样如果 n8n 挂了,PHP 会迅速失败,虽然丢数据,但至少不卡死。
第七部分:进阶玩法——让 n8n 也能写代码
n8n 最强大的地方在于它的节点。但有时候,标准的节点满足不了你变态的需求。
比如,你不想发钉钉,你想发 Slack,还想发邮件,还想发短信(阿里云/腾讯云 SMS)。如果你想写点复杂的逻辑,比如“计算库存周转率”,n8n 自带的节点可能不够。
这时候,Code 节点 就是你最好的朋友。
在 n8n 的 Flow 中,你可以插入一个 Code 节点,选择 JavaScript。
// n8n Code Node
const input = $input.item.json;
const material = input.material;
// 简单的计算逻辑
let status = 'normal';
if (material.quantity <= material.min_stock) {
status = 'critical';
}
// 返回处理后的数据
return [{
json: {
...material,
status: status,
message: status === 'critical' ? '需要立即补货' : '库存正常'
}
}];
你甚至可以在 n8n 里调用其他 API,比如查询 Google Sheets,或者调用 Python 脚本(通过 POST 请求)。
第八部分:监控与运维
建好了流,你还得会修。
- 查看运行历史:n8n 自带一个 Dashboard。点击右上角的“History”,你可以看到每条 Webhook 的执行时间、输入输出、是否成功。这就是你的运行日志。
- 重试机制:n8n 有个很棒的功能,如果某个节点失败了(比如发邮件失败),n8n 会自动重试。你可以在节点设置里调整重试次数。
- 版本控制:虽然 n8n 有个“导出工作流为 JSON”的功能,但我强烈建议你在本地开发环境弄好,然后直接覆盖 n8n 的 JSON 文件。这对于团队协作非常重要。
结语
好了,各位,今天的讲座就到这里。
我们今天用 PHP 的稳健,换来了数据库的安全;用 n8n 的灵活,换来了通知的及时。我们用几行代码,解决了一个可能会让仓库主管发疯的问题。
这就是全栈链路的魅力。你不需要精通所有技术栈,但你需要懂得如何让它们像齿轮一样啮合。
最后送大家一句话:自动化不是要取代人,而是要把人从无聊的重复劳动中解放出来,去干更创造价值的事情。 比如去喝杯咖啡,或者思考一下下周的团建去哪里。
现在,去你的服务器上,敲下那行 curl 命令,看看你的 n8n 是否亮起了绿灯吧。祝你好运,自动化大师!