EDI系统上线后最容易被忽略的一件事:文件到底发出去没有。客户那边没收到订单,或者发票少了附件,业务同事来问,你翻半天日志才发现文件卡在传输队列里已经三天了。这种事发生一次是意外,发生三次就是流程缺陷。做监控不是技术洁癖,而是必须。
标题里这个“监控文件已成功通过EDI系统发给客户”是个系列,我打算从邮件通知写起。原因很简单:邮件通知是性价比最高的一层监控,几乎适配所有EDI平台,既不依赖特定产品,也不要求外围系统有复杂接口,一套脚本能管十几条传输链路。这篇文章就把我实际落地的一套方案完整拆出来,从方案选型、目录约定、脚本逻辑到踩坑记录都写清楚,给正在做同类事情的人一个能直接抄作业的参考。
1. 先看清问题的全貌:怎样才算“成功发送”
1.1 “文件发出去了”和“发送成功”是两件事
我接触过不少EDI运维场景,刚开始监控的时候最容易犯一个认知错误:只要EDI系统界面上显示“已发送”,就觉得万事大吉。但真出了问题就会发现,“已发送”只是本地队列把文件交给了传输网关,离客户系统真正收到还隔着好几层。
一个文件从你的EDI系统出发,到客户系统落库,大致要经过几个环节:EDI系统生成文件并签名加密、通过AS2或SFTP等传输协议发送、客户侧网关接收并校验、客户的后端系统解析入库。任何一个环节掉链子,在本地界面上可能都还显示“已发送”,但客户那边就是收不到。
这也是为什么要做独立监控。监控的作用不是证明“我们发了”,而是证明“对方收到了”。
1.2 邮件通知在整个监控体系里的定位
邮件通知属于最基础、最外围的一层监控。它解决的是“结果可见性”问题:把EDI系统的发送结果主动推送给人,而不是等人登录系统去查。
相比起短信、IM机器人、电话告警,邮件有几个天然优势。第一,邮件天然适合存档,后续审计或争议时有时间戳和原始收件记录。第二,邮件不需要额外部署客户端,也无需采购短信通道,内部邮箱基本都有。第三,邮件通知是做给业务方看的,业务同事不关心AS2的MDN内容,他们只想知道“单子发出去了没有”,邮件正文直接写清楚这一点就行。
当然邮件通知也有一些不足,后面会讲到。但作为第一层监控,它投入小、见效快、几乎不需要改造现有EDI系统,这是它能作为落地方案首选的原因。
1.3 三个实现路径对比:改系统、用自带能力、外挂监控
我接触过的EDI平台大致有三类:商业软件、开源框架、自研系统。不管哪一类,做邮件通知基本离不开下面三种路径。
| 实现路径 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| EDI系统自带通知功能 | 商业软件(如部分云网关有发信通知) | 配置简单,无需写代码 | 很多版本不支持或需额外授权,通知内容固定 |
| 在EDI系统传输接口中嵌入邮件逻辑 | 自研EDI系统,代码可改 | 发送结果最准确,实时性好 | 需要动核心系统,回归测试成本高 |
| 外挂式定时轮询监控目录/日志/接口 | 几乎任何EDI系统 | 不改业务系统,独立部署独立升级,失败成本低 | 是轮询模式,实时性略差,需维护脚本 |
我自己更倾向于第三种。不是因为前两种不好,而是外挂式监控的风险最小。即便监控脚本有问题,也不会影响EDI主流程;而如果直接在传输模块里嵌邮件逻辑,一旦邮件接口故障,反而可能拖累传输进程。这个取舍在运维场景里非常关键。
2. 方案选型:为什么我选外挂轮询而不是实时监听
2.1 优先翻一翻EDI系统自己的通知配置
先说一句公道话:如果你的EDI系统本身自带发送结果通知,而且字段能满足你的需求,那就先把它用起来,不要一上来就自己写脚本。
我见过一些商业EDI网关,在传输规则里就能配置“发送成功后发送通知邮件”,也能指定收件人列表和邮件模板。这种方案零维护,直接受益于厂商升级。但实际中我遇到的大部分情况是:要么许可证不含这个模块,要么通知粒度太粗(只告诉你“今天的批处理跑完了”,不告诉你具体每个文件是否成功)。这种情况下,外挂方案就顺理成章地补位了。
2.2 外挂式定时轮询:用最笨的办法解决最实际的问题
所谓外挂式轮询,就是写一个脚本,每隔固定时间去看EDI系统留下的“痕迹”,根据痕迹判断某个文件是否已经成功发送。
这个“痕迹”在每个环境里不一样。常见的几种:
- EDI系统输出目录下有对应的发送回执文件(如
.ack、.mdn、.sent) - 文件发送后从
outbox目录被移动到sent目录 - 文件名被加上后缀或前缀,例如订单文件
PO_001.edi变成PO_001.edi.sent - 数据库日志表里写入发送完成状态
我的建议是:先看目录和文件命名的变化,再看数据库。因为看文件系统是解耦程度最高的方式,即使数据库表结构变了,只要目录约定还在,脚本就能继续工作。
2.3 选轮询而不是实时文件监听的三个理由
你可能会想,明明有文件系统监听工具,为什么不用它实现“实时触发”,非要定时轮询?
第一,稳定压倒实时。EDI传输对大多数企业来说不是毫秒级业务,文件发送完成通知晚一分钟无关紧要,但监控脚本因为监听队列积压或文件句柄冲突而崩掉,代价反而更大。
第二,轮询样本结果更容易排查。每跑一轮,日志里就有一条记录“这次检查了哪些文件,发现了什么状态”,出了问题能很快回溯。实时监听基于事件模型,要么没触发,要么触发了没日志,排查起来费劲。
第三,轮询脚本可以测。因为逻辑是线性的“查目录、判断状态、发邮件”,单测和手工验证都很直接。实时监听依赖系统事件,测试场景很难覆盖。
我实际用的轮询间隔是60秒。这个间隔对“业务确认文件已发出”的需求来说完全够用,而且不会对共享目录造成访问压力。
3. 实操:搭建一套可靠的邮件通知监控脚本
3.1 先约定监控目录和状态标记:这是整个方案的基石
动手写脚本前,第一步不是敲代码,而是确认你的EDI系统在发送成功后到底会留下什么痕迹。我和团队在实际环境里总结了一套通用的目录状态约定,分享出来供参考。
- 发送前:文件位于
EDI/outbox/,文件名保持原始格式 - 发送中:文件仍位于
EDI/outbox/,但EDI系统会写入一个同名.lock标记 - 发送成功:文件被移动到
EDI/sent/目录,文件名不变 - 发送失败:文件保留在
EDI/outbox/,同时生成.err标记文件
如果你们的EDI系统目录结构不同,也不用照搬,核心是找到四个状态的切入点。我特别建议在方案评审阶段就跟EDI负责人确认一个问题:“发送成功这个动作,系统会不会改文件名或者移动文件?”如果答案是不会,就需要再找其他信号,比如日志文件或数据库字段。
3.2 脚本核心逻辑:扫描、判定、去重、发信
下面是我在其中一个项目里实际用过的核心脚本框架,用Python写的,逻辑非常朴素但足够稳定。
import os import smtplib import json from email.mime.text import MIMEText from email.utils import formatdate from pathlib import Path OUTBOX = Path("/data/EDI/outbox") SENTBOX = Path("/data/EDI/sent") STATE_FILE = Path("/data/EDI/monitor_state.json") def load_state(): if not STATE_FILE.exists(): return set() with STATE_FILE.open("r", encoding="utf-8") as fp: return set(json.load(fp)) def save_state(done): with STATE_FILE.open("w", encoding="utf-8") as fp: json.dump(sorted(done), fp, ensure_ascii=False, indent=2) def send_mail(subject, body, to_list): msg = MIMEText(body, "plain", "utf-8") msg["Subject"] = subject msg["From"] = "edi-monitor@example.com" msg["To"] = ", ".join(to_list) msg["Date"] = formatdate(localtime=True) with smtplib.SMTP("smtp.example.com", 587) as server: server.starttls() server.login("edi-monitor", "your-password") server.send_message(msg) def main(): done = load_state() # 扫描 sent 目录中的新文件 for f in SENTBOX.glob("*.edi"): if f.name in done: continue # 提取关键字段用于邮件正文 partner = f.name.split("_")[0] send_date = f.stat().st_mtime send_mail( subject=f"[EDI监控] 文件已成功发送: {f.name}", body=f"交易伙伴: {partner}\n文件名称: {f.name}\n发送时间: {send_date}\n\n此邮件由EDI监控系统自动发出。", to_list=["ops@example.com", "business@example.com"], ) done.add(f.name) # 发现 outbox 中失败残留 for f in OUTBOX.glob("*.err"): if f.name in done: continue send_mail( subject=f"[EDI监控-异常] 文件发送失败: {f.name}", body=f"文件 {f.name} 在发送后仍留在 outbox,请立即检查EDI系统。", to_list=["ops@example.com"], ) done.add(f.name) save_state(set(done)) if __name__ == "__main__": main()几个关键点说明一下。
用monitor_state.json这样一个状态文件标记“哪些文件已经通知过了”,是为了防止重复发信。如果这个脚本每次跑都扫一遍sent目录,以前已通知过的文件会被再次触发,业务同事的邮箱会被轰炸。
扫描只用glob("*.edi"),会自动忽略.lock、.err这些临时标记文件。发送完成的文件如果刚好被移动到sent目录,那它自然会被识别到。
状态文件的读写用json,虽然简单但足够用了。如果文件量很大,可以考虑SQLite,但日常EDI文件量级完全没必要。
3.3 邮件模板设计:让收件人一眼看懂
邮件通知别整太花哨,但要足够清晰。我踩过模板设计不合理的坑——有次模板里放了一堆技术字段,业务同事根本看不懂,结果还是天天来问“到底发出去没”。
后来我把邮件模板精简成四要素:
- 动作:文件已成功发送 / 文件发送失败
- 文件类型:订单 / 发票 / 发货通知
- 文件名称:原始文件名,方便追溯
- 交易伙伴:客户名称,最好用业务称呼而非代码
实际发送效果类似这样:
[EDI监控] 文件已成功发送: PO_20240815_A001.edi 交易伙伴: 客户A 文件类型: 采购订单 文件名: PO_20240815_A001.edi 发送时间: 2024-08-15 10:24:33 发送通道: AS2 此邮件由EDI监控系统自动发出,请勿直接回复。注意,我把“发送通道”也放进去了。因为同一个交易伙伴可能同时走AS2和SFTP两条链路,出问题时知道走哪条链路能少绕一圈弯路。
3.4 部署、日志和开关控制:三个容易忽略的细节
脚本写完后,部署时建议直接做成系统服务,而不是扔个后台nohup就完事。不管是Linux的systemd还是Windows的计划任务,核心点是一样的:保证挂了能自动拉起、能写日志、能看历史状态。
我习惯的systemd单元配置大致如下:
[Unit] Description=EDI Send Monitor After=network.target [Service] Type=simple User=ediops Group=ediops ExecStart=/usr/bin/python3 /opt/edi-monitor/run.py Restart=always RestartSec=15 StandardOutput=append:/var/log/edi-monitor/run.log StandardError=append:/var/log/edi-monitor/run.log [Install] WantedBy=multi-user.target日志要分两个层次打。第一层是运行时日志,记录每次轮询执行了没有;第二层是审计日志,记录哪些文件已发过通知。审计日志又不应该只依赖邮件本身,万一邮件发送接口挂了呢?所以我会建议在发信成功后再写一条审计记录,例如落一个notified.log,格式为“时间|文件名|收件人|发送结果”,出问题时可以快速对比。
还有开关控制。运维久了你就知道,有些场景下需要临时关闭邮件通知而不停掉监控本身——比如大批量补发文件时,几百封通知邮件会淹没所有人。我推荐引入一个notification_enabled开关,放在配置文件里,通过外部信号或管理命令切换,脚本每轮启动时读一次。
config = {"notification_enabled": True} def is_notification_enabled(): return config.get("notification_enabled", False)加这么一个开关就是多几行代码的事,但能避免很多运维尴尬。
4. 日常维护:这四类坑我很确定你会遇到
4.1 重复通知和漏通知,往往是同一个原因
重复通知的根源基本都是状态记录没有做对。之前我提的state.json方案,如果实现得不够严谨,比如刚启动还没写完状态文件进程就被重启了,那已发过通知的文件很容易再次被发送。
漏通知的根源,则往往是敏感词“文件扩展名变化”。很多EDI系统发送成功后,会给文件增加一个后缀,比如.sent。如果你只监控.edi后缀的文件,移动后的.edi.sent就不会被扫到,通知自然也就没触发。后来我的脚本改成同时匹配.edi和.edi.sent,并统一按原文件名去重,才彻底解决。
4.2 邮件被拦截或进了垃圾箱
这是做过邮件通知的人都懂的痛。我们内部是自建邮箱,曾经因为邮件内容里带“订单金额”“发票”这种词,被反垃圾网关拦截了不少。后来处理分两步:一是把监控发件人地址加入白名单,二是控制邮件频率,不要短时间内发大量同主题邮件。
如果邮件需要发给外部邮箱,我的经验是直接建议用EDI平台自带的通知功能或独立的邮件发送服务,而不是自己搭的邮件服务器。自建服务器投递率受限于IP信誉,一旦被列入黑名单,恢复周期很长,监控会持续“哑火”。
4.3 时间戳的人生错觉:时区与夏令时
邮件正文里展示“发送时间”看起来很简单,但时区处理稍微疏忽就会出问题。EDI系统可能配置的是UTC,业务同事看的是北京时间或本地时间,两者相差八小时,邮件上又没标注时区,业务同事会以为文件是凌晨才发的。
我在脚本里统一用带时区信息的时间格式,比如2024-08-15 10:24:33 +08:00。如果系统支持,可以直接用%(asctime)s加时区配置,或者在邮件里写清楚是服务器本地时间。别小看这个细节,它直接影响业务同事对“文件是否及时发送”的判断。
4.4 监控脚本自己挂了,却没有被发现
这是一个终极问题:监控本身没人监控。一旦脚本宕机,你并不知道“你没收到确认邮件”这件事本身就是一个告警信号。所以建议至少做两个止损动作。
一是给监控脚本加心跳机制,比如每隔5分钟往一个状态目录里写一个心跳文件,并由另一个独立检查任务定期查看这个文件的新鲜度。
二是把“每天零点如果当天没有一条通知记录则触发异常提醒”做成一个兜底逻辑。也就是说,即便一切正常没有文件传输,也会有一封“今日无消息”的静默提醒。有了它,你就可以区分“今天没业务”和“监控坏了”两种情况。
5. 从邮件通知到监控闭环:下一步往往可以这样衔接
5.1 校验手段的升级:不只依赖目录,更要看ACK回执
邮件通知只是结果通知,它的判断依据如果只是“文件移到了sent目录”,在某些场景下还不够可靠。比如AS2协议里,只有收到对方返回的MDN(Message Disposition Notification)才算真正送达,而不仅仅是发送方这边成功写完网络。
所以如果严格要求“客户成功收到”,建议在脚本里增加一个规则:只有当sent目录中存在对应的.mdn或.ack文件时,才发成功通知;只有.edi没有.ack则进入待确认状态。这一步能过滤掉很多“半成功”状态。
5.2 自动化汇总:日报和周报能省很多“口舌”
邮件通知面向的是“当下”,日报周报面向的是“趋势”。我建议在邮件通知跑通之后,再加一个汇总任务:每天上午9点自动发送前一天的发送统计,包括成功件数、失败件数、平均耗时、异常链路清单。
这份日报的价值不能低估。很多业务问题的发现并不依赖实时告警,而是靠周期统计里暴露出的趋势异常。比如某个通道最近三天成功率从99%降到92%,这种信号在当日明细里很容易被淹没,但日报能直观暴露出来。
5.3 这个系列后续还可以扩展的方向
邮件通知是监控闭环的第一块拼图,后面还有几块值得继续做:
- 文件发送失败的自动重试机制和历史失败根因分析
- 传输耗时的异常波动告警(比如某客户平时2分钟,今天突然要40分钟)
- 文件发送成功后的业务回执关联(客户是否已确认收到并入库)
- 更下游的监控:数据落库后业务系统的处理状态追踪
这些方向其实已经超出了“文件是否发出”的范畴,进入了“业务是否闭环”的领域。但有邮件通知打底,后续每加一层监控,都能建立在已有的完整数据基础上。
根据我个人的实操体会,做邮件通知监控最关键的并不是技术本身,而是先把“成功”定义清楚,并和业务方对齐预期。我见过不少团队花大力气做了很复杂的告警平台,结果业务方对上面那个时间戳和状态定义根本不认同,最后监控系统成了一堆摆设。
另一个很小的建议:新建这类邮件通知时,收件人列表里一定要包含一个长期不用的归档邮箱。这样任何通知记录都有完整副本可查,不会因为某个人离职删邮箱,导致历史审计资料缺失。
邮件通知只是第一步,但它绝对值得认真做。这套方案不改EDI核心系统、不依赖厂商许可证、脚本逻辑半小时能讲完,却能让你从“被动查日志”翻身变成“主动收结果”,这个投入产出比,我实际操作下来觉得很划算。