Files
inkreach-prefect/flows/monitor/daily_report.py
T
yeuimu 9b130097f9 feat: 通知发送层切换到群机器人 webhook, 排版精简
- wecom.py 重写: HTTP POST webhook(替代 aibot WebSocket 按需连接), 接口 send_markdown/send_text 不变, Secret Block wecom-webhook 存 URL
- webhook markdown 支持标题/引用/font 三色, 报告改为 '## 标题 + 单行结论 + 引用详情' 紧凑排版, 结论行用 info/warning 色
- alert_failure 改标题式排版; device_check 结论用 font 色, 去掉负载行
- 移除 wecom-aibot-sdk 依赖与 wecom_capture_chatid.py(镜像下次构建生效, 无需立即重建)
2026-09-07 10:09:11 +00:00

129 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""数据核验日报(每日 18:30):RIIN/SDS 数量一致性 + 状态差异 + OCR 当日 + 失败任务回顾。
对应 daily-report deployment,cron 30 18 * * *。报告结构:
1. RIIN/SDS:先报订单数量(远程 vs 本地,有差异修复后重数)与商品行数是否一致;
再报核验时发现多少单状态不符(已自动刷新修复)。subflow 复用 riin/sds-verify。
2. OCR:今天处理了多少订单、识别失败多少(按 ocrProcessedAt 盖章统计)。
3. 失败任务:近 24h 哪些 workflow、哪些 run、失败几次、何时失败(事件报警的兜底汇总)。
消息排版遵循 WeCom aibot markdown 子集(无表格、无 font 标签,引用块 + emoji)。
"""
from __future__ import annotations
from datetime import datetime, timedelta
from prefect import flow
from flows.monitor.device_check import check_prefect_failures
from flows.riin.verify import riin_verify
from flows.sds.verify import sds_verify
from flows.shared import SyncDB, emit_sync_summary, send_markdown, start_of_day
def _affected_units(diff: dict) -> int:
"""状态不符单数 ≈ 本地比远程多的状态计数之和(远程改了状态的单)。"""
return sum(v for v in (diff or {}).values() if v > 0)
def _system_lines(name: str, report: dict, goods: dict, lines: list[str]) -> bool:
"""单系统一行结论:数量(远程 vs 本地,一致只显示一个数)+商品;状态不符进引用行。"""
vr = (report.get("verifyReport") or {}) if isinstance(report, dict) else {}
days_r = vr.get("days") or []
post = vr.get("post")
if post:
remote_t, local_t = post["remote"], post["local"]
else:
remote_t = sum(d.get("remote", 0) for d in days_r)
local_t = sum(d.get("local", 0) for d in days_r)
ok = remote_t == local_t # 数量一致只看总量;状态差异属第二段,不混入数量结论
g = goods or {}
zero = g.get("zeroGoodsOrders", 0)
goods_txt = f"{g.get('goods', 0):,} 行" + (f"(缺 {zero:,})" if zero else "")
order_txt = f"订单 {remote_t:,} ✅" if ok else f"订单 远程 {remote_t:,} / 本地 {local_t:,} ⚠️"
lines.append(f"**{name}** {order_txt} · 商品 {goods_txt} {'✅' if not zero else '⚠️'}")
diff_days = [d for d in days_r if not d.get("ok")]
diff_units = sum(_affected_units(d.get("diff")) for d in diff_days)
if diff_units:
lines.append(f"> 状态不符约 {diff_units:,} 单({len(diff_days)} 天),已自动刷新修复")
return (not ok) or bool(zero)
def build_daily_message(
days: int, window: str, riin: dict, sds: dict,
goods: dict, ocr_today: dict, ocr_pending: int, failures: list[dict],
) -> tuple[str, bool]:
"""拼装日报 markdown,返回 (消息, 是否需关注)。单行结论 + 引用详情的紧凑排版。"""
lines = [f"## 📊 数据核验日报 {datetime.now():%m-%d %H:%M}(近 {days} 天 {window})"]
attention = False
for name, report in (("RIIN", riin), ("SDS", sds)):
try:
attention |= _system_lines(name, report, (goods or {}).get(name), lines)
except Exception as err: # noqa: BLE001
lines.append(f"**{name}** 核验结果解析失败:{err}")
attention = True
processed, failed = (ocr_today or {}).get("processed", 0), (ocr_today or {}).get("failed", 0)
ocr_txt = f"**OCR 今日** 处理 {processed:,} · 识别失败 {failed:,}" + (" ✅" if not failed else " ⚠️")
if ocr_pending:
ocr_txt += f"(累计待处理 {ocr_pending:,})"
lines.append(ocr_txt)
if failures:
attention = True
lines.append(f"**失败任务(24h)** {len(failures)} 次 🔴")
for f in failures[:6]:
start = (f.get("start") or "")[5:] # MM-DD HH:MM
lines.append(f"> {f.get('flow', '?')}「{f.get('name', '?')}」{f.get('state', '?')} · {start}")
if len(failures) > 6:
lines.append(f"> …等共 {len(failures)} 次")
else:
lines.append("**失败任务(24h)** 0 ✅")
lines.append('<font color="warning">⚠️ 存在需关注项,见上</font>' if attention
else '<font color="info">✅ 数据全部一致,无异常</font>')
return "\n".join(lines), attention
@flow(name="daily-report", log_prints=True)
def daily_report(days: int = 7) -> dict:
"""近 N 天数据核验 + 商品完整性 + OCR/失败回顾,发企微群日报。"""
today = start_of_day(datetime.now())
window_start = today - timedelta(days=days)
window_end = today # 与 verify 的 recent 约定一致:近 N 天、不含今天
riin = riin_verify(mode="recent", recent_days=days)
sds = sds_verify(mode="recent", recent_days=days)
db = SyncDB()
goods = {sys: db.get_goods_integrity(sys, window_start, window_end) for sys in ("RIIN", "SDS")}
ocr_today = db.count_ocr_today(today)
ocr = db.count_ocr_status()
failures = check_prefect_failures(hours=24)
window = f"{window_start:%m-%d}~{window_end:%m-%d}"
content, attention = build_daily_message(days, window, riin, sds, goods,
ocr_today, ocr.get("pending", 0), failures)
sent = send_markdown(content)
emit_sync_summary("数据核验日报", [
("核验窗口", f"近{days}天"), ("需关注", "是" if attention else "否"),
("RIIN 零商品订单", goods["RIIN"]["zeroGoodsOrders"]),
("RIIN 商品无名称", goods["RIIN"]["goodsNoName"]),
("SDS 零商品订单", goods["SDS"]["zeroGoodsOrders"]),
("SDS 商品无名称", goods["SDS"]["goodsNoName"]),
("全库孤儿商品", goods["RIIN"]["orphanGoods"]),
("OCR 今日处理", ocr_today.get("processed", 0)), ("OCR 今日失败", ocr_today.get("failed", 0)),
("OCR 待处理", ocr.get("pending", 0)), ("失败 runs(24h)", len(failures)),
("已发企微", "是" if sent else "否"),
])
return {"attention": attention, "sent": sent,
"goods": goods, "ocrToday": ocr_today, "failures24h": len(failures)}
if __name__ == "__main__":
daily_report.serve(name="daily-report", cron="30 18 * * *")