"""每日数据找人推送 — 路线图R2 (2026-08-30) 北极星③:主动推送扩大 —— 异常 + 机会两类。 - 异常类:待处理预警(kpi_alerts pending) - 机会类:KPI向好 / 预算余量 / 预测上行(opportunity_detector) 复用企微通道 8800/send(公司群中继服务)。 用法: /root/cma-management/backend/venv/bin/python3 scripts/daily_push.py [--dry-run] cron: 15 9 * * * (alert_generator 9:00 之后) """ import sys import os import json import logging import argparse from datetime import datetime sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app.database import get_session_local from app.models import KPIDefinition, KPIAlert from scripts.opportunity_detector import detect_all, flatten logger = logging.getLogger("cma.daily_push") RELAY_URL = "http://127.0.0.1:8800/send" SOURCE = "管理会计OS" def collect_exceptions(db, limit: int = 10) -> list: """异常类:待处理预警(red/yellow)""" out = [] alerts = db.query(KPIAlert).filter( KPIAlert.status == "pending", KPIAlert.alert_level.in_(["red", "yellow"]), ).order_by(KPIAlert.created_at.desc()).limit(limit).all() for a in alerts: k = db.query(KPIDefinition).filter(KPIDefinition.id == a.kpi_id).first() kpi_name = k.kpi_name if k else f"KPI#{a.kpi_id}" icon = "🔴" if a.alert_level == "red" else "🟡" out.append({ "type": "exception", "title": f"{icon} {kpi_name} 预警", "detail": f"({a.alert_level}) {a.alert_message}", "kpi_id": a.kpi_id, "kpi_name": kpi_name, "period": "", }) return out def build_message(exceptions: list, opportunities: list) -> str: """组装 markdown 推送内容""" now = datetime.now().strftime("%Y-%m-%d %H:%M") lines = [f"## 📊 管理会计OS · 每日经营播报", f"**{now}**", ""] lines.append("### ⚠️ 异常关注") if exceptions: for e in exceptions: lines.append(f"- {e['title']}") lines.append(f" {e['detail']}") else: lines.append("- 今日无待处理预警 ✅") lines.append("") lines.append("### 🎯 机会发现") if opportunities: for o in opportunities: lines.append(f"- {o['title']}") lines.append(f" {o['detail']}") else: lines.append("- 今日暂无显著机会") lines.append("") lines.append("---") lines.append("💡 数据找人:异常要处理,机会要把握。详情见 CMA 系统。") return "\n".join(lines) def push_wecom(msg: str) -> dict: """通过8800中继推送企微""" import urllib.request import urllib.parse data = urllib.parse.urlencode({ "msg": msg, "source": SOURCE, "msgtype": "markdown", }).encode("utf-8") req = urllib.request.Request(RELAY_URL, data=data, headers={"Content-Type": "application/x-www-form-urlencoded"}) try: with urllib.request.urlopen(req, timeout=15) as resp: result = json.loads(resp.read().decode("utf-8")) return result except Exception as e: return {"ok": False, "error": f"推送异常: {e}"} def main(): parser = argparse.ArgumentParser() parser.add_argument("--dry-run", action="store_true", help="只打印不推送") parser.add_argument("--entity-id", type=int, default=1) args = parser.parse_args() db = get_session_local()() try: exceptions = collect_exceptions(db) opportunities = flatten(detect_all(db, args.entity_id)) msg = build_message(exceptions, opportunities) if args.dry_run: print(msg) print(f"\n[DRY-RUN] 异常{len(exceptions)}条 / 机会{len(opportunities)}条") return result = push_wecom(msg) print(f"推送结果: {json.dumps(result, ensure_ascii=False)}") print(f"统计: 异常{len(exceptions)}条 / 机会{len(opportunities)}条") finally: db.close() if __name__ == "__main__": logging.basicConfig(level=logging.INFO) main()