目录
-
- 一、功能目标
- 二、下降沿登记任务
- 三、定时检查
- 四、配置共享目录与机器人KEY
- 五、查找最新PDF
- 六、PDF上传与发送
- 七、最终架构
- 八、现场复用清单
- 九、注意事项
-
- [1. 为什么上传后还要再发送一次](#1. 为什么上传后还要再发送一次)
- [2. 没有PDF时不发送](#2. 没有PDF时不发送)
- 十、最终效果
一、功能目标
PLC下降沿登记任务,次日9点推送PDF报告
实验结束时,PDF 报告通常还没有生成。本项目中,PDF 由其他系统在凌晨自动生成,然后放到共享目录。
因此不能在 PLC 下降沿时立刻发送 PDF,而是:
text
下降沿
↓
登记PDF发送任务
↓
第二天9点检查共享目录
↓
找到当天最新PDF
↓
发送到企业微信群

注意:企业微信发送文件不是一步完成的。
必须先上传文件拿到 media_id,再用 media_id 发送文件消息。
text
PDF文件路径
↓
读取PDF二进制内容
↓
upload_media接口上传
↓
得到media_id
↓
webhook/send接口发送file消息


二、下降沿登记任务
新增 PDF发送任务登记 节点,和文本消息构造节点并联:
text
边沿检测
├── 消息构造
└── PDF发送任务登记
关键代码:
js
if (msg.edgeType !== "falling") {
return null;
}
// 在流上下文里登记一个【待发送任务】。
// 第二天早上9点的定时节点会读取这个标志。
flow.set('pdfSendTask', {
pending: true,
fallingTime: new Date().toISOString(),
mode: flow.get('mode') || 0
});
return null;
这里不直接发送文件,只生成一个任务:
text
pending = true
第二天 9 点检查任务时,只要看到 pending 为 true,才继续查找 PDF。
三、定时检查
新增一个 inject 节点:
text
节点名称:每天9点检查PDF发送
crontab:00 09 * * *
topic:pdf_send_check
如果 PDF 生成可能延迟,可以把时间改成:
text
05 09 * * *
10 09 * * *
四、配置共享目录与机器人KEY
使用流程环境变量
text
PDF_SHARE_DIR:<PDF报告共享目录>
WECHAT_BOT_KEY:<企业微信机器人KEY>
注意:在 Node-RED 流程页签里配置两个环境变量(双击流程标签):

读取方式:
js
const SHARE_DIR = env.get('PDF_SHARE_DIR');
const WECHAT_KEY = env.get('WECHAT_BOT_KEY');
容易写错的地方:
js
// 错误:env.get里面不是写路径
const SHARE_DIR = env.get('<PDF报告共享目录>');
// 正确:env.get里面写变量名
const SHARE_DIR = env.get('PDF_SHARE_DIR');
五、查找最新PDF
检查逻辑:
- 没有待发送任务,直接结束。
- 有任务,扫描共享目录。
- 只保留当天创建的 PDF。
- 按创建时间倒序。
- 取最新的一个。
- 如果没有 PDF,清掉任务,不发送。
核心代码:
js
// 1. 读取配置:共享文件夹路径和企业微信机器人key。
const SHARE_DIR = env.get('PDF_SHARE_DIR');
const WECHAT_KEY = env.get('WECHAT_BOT_KEY');
// 2. 只有下降沿登记过任务时,早上9点才继续处理。
const task = flow.get('pdfSendTask');
if (!task || !task.pending) {
node.status({ fill: "grey", shape: "ring", text: "无待发送任务" });
return null;
}
// 3. 把Date对象格式化成 yyyy-MM-dd,用来判断PDF是不是今天创建的。
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 4. 配置没填时直接报错,不继续访问共享目录。
if (!SHARE_DIR) {
node.error('请先配置 PDF_SHARE_DIR', msg);
return null;
}
if (!WECHAT_KEY) {
node.error('请先配置 WECHAT_BOT_KEY', msg);
return null;
}
// 5. 扫描共享目录:只保留今天创建的PDF,并按创建时间倒序排列。
const files = fs.readdirSync(SHARE_DIR)
.filter(name => name.toLowerCase().endsWith('.pdf'))
.map(name => {
const fullPath = path.join(SHARE_DIR, name);
const stat = fs.statSync(fullPath);
return { name, fullPath, stat };
})
.filter(file => file.stat.isFile() && formatDate(file.stat.birthtime) === today)
.sort((a, b) => b.stat.birthtimeMs - a.stat.birthtimeMs);
// 6. 没有PDF就清掉任务,不发企业微信,也不反复等待。
if (files.length === 0) {
flow.set('pdfSendTask', {
...task,
pending: false,
skippedAt: new Date().toISOString(),
reason: 'no_pdf_created_today'
});
return null;
}
六、PDF上传与发送
PDF 文件发送已经单独调试完成,这里只记录架构,不重复展开细节。
企业微信文件发送分两步:
text
1. upload_media 上传PDF,得到 media_id
2. webhook/send 用 media_id 发送 file 消息
架构:
text
查找最新PDF并准备上传
↓
上传PDF到企业微信
↓
构造PDF文件消息
↓
消息队列
↓
企业微信HTTP发送
文件消息格式:
json
{
"msgtype": "file",
"file": {
"media_id": "<上传接口返回的media_id>"
}
}
上传成功后,要清理临时 HTTP 字段,避免影响后面的发送节点:
js
delete msg.method;
delete msg.url;
delete msg.headers;
七、最终架构
完整结构:
text
S7 in
↓
switch变量分流
├── Mode变量 -> flow.set('mode')
└── EndingFlag -> 边沿检测
├── 消息构造 -> 消息队列 -> 企业微信发送
└── PDF发送任务登记
每天9点inject
↓
查找最新PDF
↓
上传PDF
↓
构造文件消息
↓
消息队列
↓
企业微信发送
八、现场复用清单
下次复用时,只需要重点检查:
- PLC 连接参数是否正确。
- PLC 变量地址是否正确。
msg.topic分流名称是否和 S7 节点变量名一致。- 企业微信机器人 key 是否换成目标群。
【5】.PDF 共享目录是否能被 Node-RED 运行账号访问。
- 定时发送时间是否符合 PDF 实际生成时间。
- 如果一天可能多次实验,需要把
pdfSendTask从单对象扩展为队列。
九、注意事项
1. 为什么上传后还要再发送一次
企业微信文件消息分两步:
第一步:上传文件到企业微信临时素材。
text
upload_media
作用是把文件传给企业微信,返回 media_id。
第二步:上传得到 media_id,发送文件消息到群里。
json
{
"msgtype": "file",
"file": {
"media_id": "xxxx"
}
}
作用是把这个文件真正发送到群里。
所以"上传PDF到企业微信"不等于"群里已经收到文件"。
2. 没有PDF时不发送
早上 9 点如果没有找到当天创建的 PDF:
- 清掉
pdfSendTask.pending - 不发送企业微信
- 不报错刷屏
这样更适合现场长期运行。
十、最终效果
实验结束时:
text
PLC下降沿 -> 登记PDF发送任务
第二天早上:
text
9:00 -> 检查共享目录 -> 找到最新PDF -> 上传企业微信 -> 发送文件到群
如果当天没有 PDF:
text
9:00 -> 检查共享目录 -> 未找到PDF -> 清掉任务 -> 不发送