1. 先想清楚:这类需求的真实形态和三条推送技术路线
1.1 看似都是“新记录推企微”,其实背后是四种完全不同的业务场景
刚接到“监控数据表中的新记录并自动推送到企业微信群”这个需求时,千万别急着写代码。先搞清楚对方到底在监控什么表、推完之后要干什么,因为“新记录”这三个字在不同系统里的含义完全不一样:
- 电商/订单场景:订单表插入一条新订单,群里的运营或者客服需要第一时间看到,点击进去核单、审单、处理异常。
- 工单/审批场景:工单表插入新工单,需要提醒值班人员领取任务,甚至直接在企业微信里完成派单流转。
- IoT/设备告警场景:设备上报一条告警事件,群里收到提醒后需要快速判断是否升级、是否通知现场处理。
- 数据采集/导入场景:定时任务批量导入了上万条数据,数据表有新增,需要给相关方发一个汇总通知,而不是逐条轰炸。
这四种场景对实时性、消息格式、后续动作的要求差别很大。订单场景要求秒级响应,汇总场景一分钟一次也能接受;告警场景要求消息里带上级别和跳转详情页入口,工单场景则要求推送完之后还能走完一个“处理流程”。所以第一步永远不是选技术,而是把需求翻译成三个问题:监控哪张表、什么算新记录、推完之后人要干什么。
1.2 企业微信推送有三条路线,选错一条后面全得返工
企业微信的推送能力不是只有一种,我见过太多人一开始图省事全堆在群机器人上,做到一半发现没法点链接、没法关联审批,又推倒重来。三条路线对应的能力和代价差别很明显:
| 路线 | 核心能力 | 接入复杂度 | 典型使用场景 |
|---|---|---|---|
| 群机器人 Webhook | 向指定群推送文本/Markdown/图片/文件,支持@成员 | 极低,拿到Webhook地址即可 | 纯粹的提醒通知、告警播报 |
| 自建应用消息 | 按成员/部门/标签推送,可跳转H5/小程序,能拿到用户身份 | 中等,需要CorpID、Secret、AgentId,配置可信域名 | 需要点进详情页操作、处理工单 |
| 企业微信审批API | 创建审批实例,走公司审批流,自动留痕 | 较高,需要管理端审批应用权限 | 需要人工审核、多人会签的场景 |
如果你只是“群里有条记录出来了”,群机器人是最合适的;如果要求“点这条消息打开订单详情页去操作”,那就得用自建应用消息或者群机器人里的图文卡片配合公网H5;如果整个流程必须留痕、必须走审批,那就直接对接审批API。这节不展开具体参数,后面第3节和第4节会分别给出实操代码。
1.3 动手之前先定三个设计目标
我每次帮团队做这种监控推送服务,都会先逼着对方定三个指标,不然中途必然扯皮:
第一是可容忍延迟。有些业务要求5秒内必须提醒,有些30秒轮询一次已经很奢侈。延迟要求直接决定了你用轮询还是用Binlog监听,后面会细说。第二是容错级别。漏掉一条新订单的代价有多大?如果只是漏一条没太大关系,那程序可以做得简单很多;如果一条都不能漏,就得上消息确认、状态持久化、失败重推。第三是消息量上限。平时一天几十条和一天几万条,推送侧的限流策略完全不同,企业微信群机器人每分钟只能发20条,批量大时必须合并发送。
这些目标先和需求方对齐,再往后选型,才不容易返工。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据表新记录监控:轮询、触发器、CDC三种实现怎么选
2.1 轮询方案:代码量最小,但搞清楚边界再上
轮询的原理最简单:每隔几秒执行一次查询,找出“上次处理之后新增的记录”。
一般的写法是记录上次处理的最大主键ID,每次查询WHERE id > last_id ORDER BY id ASC LIMIT n,处理完一批就更新last_id。成本低、侵入性小,不需要改数据库结构,也不需要引入新组件,这是它最受欢迎的原因。
但这个方案有两个坑必须提前知道。第一个坑是依赖自增主键连续性。如果业务代码里手动插入过指定ID的数据、或者做过数据回滚和批量导入,主键ID的“大小顺序”和“插入时间顺序”就可能不一致。第二个坑更隐蔽:在多个事务并发写入时,主键大的事务可能先提交,主键小的事务后提交。如果查询结果先看到了主键101,把last_id推进到101,主键100的行才提交,那这一条就永远漏掉了。
要降低这个风险,推荐的做法是在SQL里加一个时间兜底窗口,或者把created_at也纳入判断条件。比如查询改成:
sql复制SELECT id, order_no, amount, created_at
FROM orders
WHERE id > %(last_id)s
AND created_at < NOW() - INTERVAL 2 SECOND
ORDER BY id ASC
LIMIT %(batch_size)s
这样最近两秒内新插入但尚未最终确认的行不会被立刻消费,给并发事务留一个提交窗口。代价是增加了最多2秒的延迟,但对绝大多数推送提醒场景完全可接受。
2.2 触发器+队列表:可控性居中,适合你说了算的库
如果担心轮询漏数据,又不想引入重组件,可以在数据库里做触发器,把新记录写进一张队列表,程序只消费队列表:
sql复制CREATE TRIGGER trg_order_after_insert
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
INSERT INTO table_monitor_queue(table_name, pk_id, create_time)
VALUES ('orders', NEW.id, NOW());
END;
然后监控程序就是定时SELECT ... FROM table_monitor_queue WHERE processed = 0 ORDER BY id。因为触发器插入队列表的时机和业务写入是同一个事务,所以只要业务数据提交了,队列表里就一定有记录,基本不会漏。
但触发器方案也有代价。它会在数据库实例上增加写入开销,高并发表上触发器数量一多,对主库性能有影响;而且每次加表监控都要新写一条触发器,数据库变更需要走脚本审核,很多公司这一步卡得非常严。如果你自己就是数据库的owner,触发器是个不错的中间选择;如果还需要DBA批准,我建议先掂量一下沟通成本。
2.3 Binlog/CDC方案:专业但重,适合监控全变更
轮询和触发器只能看到“插入后的结果”,看不到update、delete,也拿不到变更前后对比。如果业务对账、审计需要完整的变更流,就得考虑基于Binlog的CDC方案,常见的有Canal、Debezium、Flink CDC。
CDC的核心机制是把自己伪装成数据库的从库,消费Binlog日志,数据只要在数据库里发生任何变更,几乎实时流转出来。严格意义上它不只是“监控新记录”,而是完整的change data capture。这个方案实时性最强、信息量最大,但部署运维成本也明显高,需要维护一个中间件服务、需要处理Binlog位点、需要关心server-id冲突,还有一套消费进度的管理。
基于我的经验,如果只是“新记录推企微群”这种需求,除非你的表写入量很大、或者消息推送只是整个数据处理链路的一环,否则CDC属于大炮打蚊子。多数中小团队把轮询做好做稳,已经能覆盖99%的需求。
2.4 我的选型结论
| 因素 | 轮询 | 触发器+队列 | Binlog/CDC |
|---|---|---|---|
| 实时性 | 秒级 | 秒级 | 亚秒级 |
| 对业务表侵入 | 无 | 需要触发器 | 无 |
| 能感知的变更类型 | Insert为主 | Insert为主 | Insert/Update/Delete全量 |
| 运维成本 | 最低 | 中 | 高 |
| 漏数据的概率 | 需要边界处理 | 低 | 低 |
| 适合场景 | 提醒推送、轻量通知 | 有一定精确性要求 | 数据同步、审计、复杂管道 |
如果让我推荐一个“大多数时候都能用”的方案,我愿意选轮询加时间兜底窗口。它足够简单,能解决90%的业务提醒问题,而且在第5节你会看到,完整代码也就一百多行。
3. 企业微信群机器人接入实操:从建群到Markdown消息送达
3.1 五分钟建机器人,拿到Webhook地址
在企业微信里建群机器人非常快:打开目标群,点击右上角设置,找到“群机器人”,添加一个机器人,给它起个名字,然后复制Webhook地址。地址形如:
code复制https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-key
这一步最容易被忽略的是安全设置。创建机器人时可以设置关键词、加签和IP限制。如果不设任何保护,只要有人拿到了Webhook地址,就可以往你的群里塞垃圾消息。我的习惯是至少开启“加签”,这样推送请求里要额外带一个用密钥计算出的签名,即使地址泄漏,别人也算不出合法签名。
加签的逻辑是:使用当前时间戳拼接密钥,用HMAC-SHA256算法计算,再做Base64编码,请求时带上timestamp和sign参数。Python代码如下:
python复制import time
import base64
import hmac
import hashlib
def build_signed_webhook(webhook_url: str, secret: str) -> str:
timestamp = str(int(time.time()))
string_to_sign = f"{timestamp}\n{secret}"
hmac_code = hmac.new(
secret.encode(),
string_to_sign.encode(),
digestmod=hashlib.sha256
).digest()
sign = base64.b64encode(hmac_code).decode()
return f"{webhook_url}×tamp={timestamp}&sign={sign}"
另外千万记得,Webhook地址是敏感信息,别直接写死在代码仓库里,我见过有人把代码传到Git仓库然后被扫描工具扫出来,最后只能紧急重置密钥。
3.2 文本和Markdown消息,别把格式用错
最简单的消息格式是text:
json复制{
"msgtype": "text",
"text": {
"content": "新订单提醒:订单号 #20240001 已生成",
"mentioned_list": ["@all"]
}
}
但实际使用中我几乎都用markdown格式,因为可以加粗、加引用、标颜色,信息密度高很多:
json复制{
"msgtype": "markdown",
"markdown": {
"content": "**订单提醒** <font color=\"warning\">需处理</font>\n> 单号:#20240001\n> 金额:¥288.00\n> 时间:2024-06-01 10:23:11"
}
}
企业微信的Markdown是子集,支持标题、加粗、引用、链接、字体颜色(info/comment/warning),但表格、图片Markdown语法不支持。还要注意一条消息的字节数限制:文本消息正文不超过2048字节,Markdown消息正文不超过4096字节。很多批量导入场景一生成就是一长串,超了会被企微拒收,程序里要做好截断。我常用的方式是把超过4000字符的内容拆成多条,或者把核心摘要作为正文,完整明细放到附带的文件消息里。
3.3 限流和超时机制:不处理必炸
企业微信群机器人官方的频控是每个机器人每分钟最多20条消息,这个限制看着宽裕,真出事时特别容易触顶。比如程序一次性处理了500条新数据,如果逐条推送,还没推到第30条就会被限流,后面全部失败,然后陷入重试风暴。
应对办法是两个:一是合并推送,把同一批次的数据拼成一条汇总消息;二是做本地漏桶,控制发送速率不超过每分钟15条,人为留出裕量。超时也要显式设置,企业微信接口偶发变慢,不设超时可能导致你的监控线程全部卡死。一般connect_timeout设3秒,read_timeout设10秒比较合适。
3.4 防误报和防重复,消息比代码更讲究
监控服务跑起来之后,最怕的不是“不报警”,而是“乱报警”。特别是数据库表里经常有测试数据、初始化数据、历史数据回填,这些记录一插入,群里就会收到一条看似正式的消息,时间久了大家就麻木了。
我的经验是加三层过滤。第一层是SQL层过滤,只监控业务相关的状态字段,比如订单表只取status = 1的记录,测试账户、内部账号直接在查询条件里排除。第二层是消息去重,给每条记录算一个特征值(比如订单号),同一特征值在一定时间窗口内只推送一次。第三层是消息内容里明确标注环境,测试环境推送时在标题上加【测试】,正式环境【生产】,避免误把测试消息当生产告警去处理。
4. 把“通知”升级为“处理流程”:卡片跳转与审批API两条路
4.1 需求分水岭:你只需要提醒,还是需要闭环处理
如果推送完,人只负责看一眼,那群机器人足够。但凡业务上要求“看完这个提醒之后,点一下去处理”“处理完要留痕”,就必须考虑第二条链路。这里的分水岭是:人在收到消息之后,能不能在不跳出企业微信的前提下完成后续操作。
我见过很多团队一开始用群机器人推送,结果消息里写“请登录后台处理”,用户还得打开PC端、找到后台、输账号密码,体验特别差,最后没人愿意点。真正的闭环应该是:群消息点开→直达处理页面→一键确认/审批→状态回写数据库。要做到这一步,有下面两条路线。
4.2 方案A:消息卡片带链接,直接拉起H5处理页
群机器人的news图文消息类型支持跳转链接。构造消息时带上url字段,群里收到一条带标题、摘要、缩略图的卡片,点击就能打开你的H5页面:
json复制{
"msgtype": "news",
"news": {
"articles": [
{
"title": "订单 #20240001 需要处理",
"description": "实付金额与订单金额不一致,请点击处理",
"url": "https://your-domain.com/orders/20240001",
"picurl": "https://your-domain.com/img/order-icon.png"
}
]
}
}
这个方案对后端要求最小,你只需要保证H5页面能被企业微信内置浏览器正常访问。但有个隐藏前提:如果用的是自建应用消息而不是群机器人,H5页面必须配置在企业微信的“可信域名”里,否则点击打开会报域名未校验。群机器人里的news消息跳转限制相对宽松,但也要保证链接的域名有正规HTTPS证书,别用IP直连。
4.3 方案B:对接企业微信审批API,直接发起处理流程
如果公司已经启用了企业微信的“审批”应用,可以把推送和处理流程打通:监控服务发现异常记录后,自动创建一个审批实例,指定审批人,审批人在企业微信的“工作台-审批-待处理”里就能看到,处理结果可以回调到业务系统。
实现要分三步。第一步获取企业微信的access_token:
python复制import requests
def get_access_token(corp_id: str, corp_secret: str) -> str:
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
params = {"corpid": corp_id, "corpsecret": corp_secret}
resp = requests.get(url, params=params, timeout=10).json()
if resp.get("errcode") != 0:
raise RuntimeError(f"获取access_token失败: {resp.get('errmsg')}")
return resp["access_token"]
access_token的有效期是7200秒,要全局缓存并定时刷新,不要每次请求都去获取,否则会碰企业微信的频率限制。
第二步通过审批模板详情接口拿到模板里的控件ID,不同审批模板的控件字段是动态的,直接用别人博客里的ID必挂。大致请求长这样:
python复制def get_template_detail(access_token: str, template_id: str):
url = "https://qyapi.weixin.qq.com/cgi-bin/oa/approval/get_template_detail"
return requests.post(
url,
params={"access_token": access_token},
json={"template_id": template_id},
timeout=10
).json()
第三步提交审批实例:
python复制def submit_approval(access_token: str, template_id: str, creator: str, contents: list):
url = "https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent/submit"
payload = {
"creator_userid": creator,
"template_id": template_id,
"use_template_approver": 1,
"apply_data": {
"contents": contents
},
"summary_list": [
{
"summary_info": [
{"text": "订单异常待审批", "lang": "zh_CN"}
]
}
]
}
return requests.post(
url,
params={"access_token": access_token},
json=payload,
timeout=10
).json()
这里use_template_approver设为1,表示审批人按模板默认配置走。如果想动态指定审批人,要改用use_custom_approver并构造approver节点,逻辑会复杂一些,但原理都一样。
4.4 两条路线怎么选
我的判断标准很简单:只要处理动作是“一个人判断一下就行”,用卡片跳转H5最省事;只要处理动作需要多人会签、需要留痕审计、需要和公司现有审批流打通,就走审批API。两条路线还可以组合,先推卡片消息,点击卡片进入H5,H5里再引导用户去发起审批,兼顾了到达率和流程合规。
5. Python端到端实现:增量识别、去重保障与消息推送完整代码
5.1 项目结构:保持简单,别一上来就上框架
很多初学者一听到这种需求就想去搭Celery、Redis、消息队列,我建议先克制。单机轮询加一个推送脚本能解决绝大多数场景,等确认有并发和水平扩展需求再架构演进不迟。我用的项目结构如下:
code复制order-monitor/
├── .env.example # 环境变量样例
├── requirements.txt # pymysql requests python-dotenv
├── config.py # 配置读取
├── state.py # 本地状态持久化
├── wecom.py # 企业微信推送封装
└── monitor.py # 主循环
5.2 状态管理:用一个JSON文件保证断点续跑
推送服务最怕重启后从头扫表。我的做法是把进度存成本地JSON文件,每处理完一批就原子写入一次,这样服务崩溃重启后可以从上次进度继续跑,不会重复推送已完成的数据,也不会从头扫一遍老数据。
python复制import os
import json
def load_state(path: str) -> dict:
if not os.path.exists(path):
return {"last_id": 0}
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def save_state(path: str, state: dict) -> None:
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(state, f, ensure_ascii=False, indent=2)
os.replace(tmp, path)
注意我用了os.replace,它会先写临时文件再原子替换,避免写一半程序崩溃把文件搞坏。如果后面服务要横向扩展成多机部署,再把这个状态迁到Redis里,当前阶段不要过度设计。
5.3 主监控循环:增量查询、批量推送、进度推进
主循环的代码贴在下面,注释写清楚了每一步在干什么:
python复制import time
import logging
import pymysql
import pymysql.cursors
from config import DB_CONFIG, WEBHOOK_URL, POLL_INTERVAL, BATCH_SIZE, STATE_FILE
from state import load_state, save_state
from wecom import send_markdown
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
def fetch_new_records(last_id: int):
conn = pymysql.connect(**DB_CONFIG)
try:
with conn.cursor(pymysql.cursors.DictCursor) as cur:
sql = """
SELECT id, order_no, amount, created_at
FROM orders
WHERE id > %s
AND created_at < NOW() - INTERVAL 2 SECOND
ORDER BY id ASC
LIMIT %s
"""
cur.execute(sql, (last_id, BATCH_SIZE))
return cur.fetchall()
finally:
conn.close()
def format_batch(rows: list) -> str:
lines = ["**新订单提醒**"]
for row in rows:
lines.append(
f"> 单号:{row['order_no']},金额:{row['amount']},时间:{row['created_at']}"
)
return "\n".join(lines)
def main():
state = load_state(STATE_FILE)
logging.info("监控服务启动,初始last_id=%d", state["last_id"])
while True:
try:
rows = fetch_new_records(state["last_id"])
if rows:
content = format_batch(rows)
send_markdown(WEBHOOK_URL, content)
state["last_id"] = rows[-1]["id"]
save_state(STATE_FILE, state)
logging.info("已推送%d条新记录,last_id更新为%d", len(rows), state["last_id"])
except Exception:
logging.exception("监控主循环异常,稍后重试")
time.sleep(POLL_INTERVAL)
if __name__ == "__main__":
main()
wecom.py里封装签名和发送:
python复制import time
import base64
import hmac
import hashlib
import requests
def send_markdown(webhook_url: str, content: str, secret: str = "") -> dict:
url = webhook_url
if secret:
timestamp = str(int(time.time()))
string_to_sign = f"{timestamp}\n{secret}"
hmac_code = hmac.new(
secret.encode(),
string_to_sign.encode(),
digestmod=hashlib.sha256
).digest()
sign = base64.b64encode(hmac_code).decode()
url = f"{webhook_url}×tamp={timestamp}&sign={sign}"
payload = {
"msgtype": "markdown",
"markdown": {"content": content[:4000]}
}
resp = requests.post(url, json=payload, timeout=10)
resp.raise_for_status()
result = resp.json()
if result.get("errcode") != 0:
raise RuntimeError(f"企业微信推送失败: {result.get('errmsg')}")
return result
5.4 这段代码的边界和可靠性说明
上面的代码能直接跑,但它是有边界条件的,你心里要有数。第一,它是“至少一次”推送语义:如果推送成功后程序在保存状态前崩溃,重启后同一批数据会被再推一遍。要完全去重,需要维护一张处理流水表记录每个主键ID,成本高一些,但设计思路就是我前面说的:先用主键ID去重窗口做短期防重,再把生产环境要求提到“精确一次”。
第二,SQL里的INTERVAL 2 SECOND是给并发事务留提交窗口,这个值不是死的。如果你们库的写入事务经常超过2秒,可以调到5秒甚至10秒,代价就是监控延迟变大。
第三,如果一批记录超过50条,当前代码会分多次查询、多次推送,但因为状态推进只在每批推送成功后执行,中途失败会重试同一批,所以不会丢,但可能出现部分重复。
另外强烈建议给每条消息加一个trace_id或者带着订单号的上下文,后面收到用户反馈“我收到了重复消息”时,能根据消息内容快速定位是哪一批、哪条记录导致的。
6. 部署到Linux后会踩的坑:时区、限流、守护与手动补偿
6.1 别用nohup裸跑,systemd才是正经守护方式
很多人第一次部署Python脚本,习惯nohup python3 monitor.py &,一旦服务器重启、或者Python进程因为内存问题被杀,监控就静默停了。正确的做法是用systemd托管,写一个service文件:
ini复制[Unit]
Description=order-monitor
After=network-online.target
[Service]
User=deploy
WorkingDirectory=/opt/order-monitor
ExecStart=/usr/bin/python3 /opt/order-monitor/monitor.py
Restart=always
RestartSec=10
EnvironmentFile=/opt/order-monitor/.env
[Install]
WantedBy=multi-user.target
然后执行:
bash复制sudo systemctl daemon-reload
sudo systemctl enable --now order-monitor
Restart=always保证进程崩溃后10秒自动拉起,EnvironmentFile把密钥从代码里挪到环境文件,User=deploy避免用root跑业务脚本。查看日志直接用journalctl -u order-monitor -f,比你自己写的日志文件方便多了。
6.2 时区不一致导致漏监控,这是最容易踩的坑
数据库里的created_at是UTC,而你的服务器时区是东八区,如果程序用本地时间做对比,两个时间一换算,查询窗口就会整体偏移8小时。我踩过一次非常狼狈:监控服务从某一天开始,每天固定漏掉早上8点到8点半之间插入的记录,排查半天最后才发现是数据库连接会话的时区没设置,NOW()返回的是UTC时间,和服务器本地时间差了8小时。
规避办法是统一时区。在DB_CONFIG里设置连接init_command:
python复制DB_CONFIG = {
"host": "127.0.0.1",
"port": 3306,
"user": "monitor",
"password": "your-password",
"database": "app",
"charset": "utf8mb4",
"init_command": "SET time_zone = '+08:00'"
}
这样数据库会话里的NOW()和程序在同一条时间线上。字符集也务必用utf8mb4,否则推送内容里的中文可能乱码。
6.3 大批量数据回填时,限流和合并策略必须有
业务方可能白天正常跑,到了晚上批量导入历史数据,一次性插入几千条。如果你的监控脚本没有合并策略,就会疯狂撞企业微信每分钟20条的限流,然后日志里全是错误。稳妥的做法是设置一个批次上限:如果一批记录超过20条,不逐条推送,而是拼成一条汇总消息,标题写“新增记录500条,点击查看详情”,并且把明细写入一个临时文件,用企业微信的file消息类型推送给群,群里点开就能下载。
我还会在服务里加一个简单的“限速器”,控制推送间隔。就算真有大批量数据,也按每分钟16条的节奏发,留点余量给其他业务消息。
6.4 手动补偿工具,关键时刻能救命
自动化服务跑久了,总会有那么一两次因为数据库连接被拒、企微接口抖动导致消息没送出去。所以我会额外维护一个backfill.py脚本,支持按主键区间手动补推:
bash复制python3 backfill.py --start-id 10000 --end-id 10500
脚本原理就是复用fetch_new_records的查询,但把时间兜底窗口关掉,强制按主键区间扫描,然后把原消息再发一遍。补推时消息内容里要标记“【补推】”,防止收到的人误以为又是新的漏单。这个工具平时不起眼,但真遇到半夜数据库故障恢复后,它能让你快速把积压的消息补出去,不用熬夜写临时脚本。
还有一个事容易被忽略:如果你用了自建应用消息里的H5链接,务必把链接里的域名配置成企业微信后台的可信域名,并且要用HTTPS。我见过有人整个服务都搭好了,最后消息里的链接一打开就报“企业微信未校验域名”,整个处理流程直接断在最后一步。这个小问题,能让人一夜回到解放前。
