Files
cma-management/backend/scripts/okr_review_reminder.py
T

120 lines
5.2 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.
# -*- coding: utf-8 -*-
"""OKR 季度复盘提醒(缺口③:周期复盘节奏机制)
用途:每季度第 2 个月中旬(2/5/8/11 月 15 日 09:00)提醒 owner 更新 objectives.progress / confidence。
- 只读 objectives 表(status='active'),不改任何目标值(本单先只做"提醒")
- 通道复用:与 backend/scripts/daily_push.py 同一企微中继(RELAY_URL=http://127.0.0.1:8800/send,SOURCE=管理会计OS)
发送记录复用既有 notification_logs 表(channel=wecom)留存审计
- 默认 dry-run(只打印,不推送);--push 才真正发送
用法:
cd /root/cma-management/backend && PYTHONPATH=. /usr/bin/python3 scripts/okr_review_reminder.py # dry-run
cd /root/cma-management/backend && PYTHONPATH=. /usr/bin/python3 scripts/okr_review_reminder.py --push # 推送
... --entity-id 1 # 仅某企业
"""
import argparse
import json
import os
import sys
import urllib.parse
import urllib.request
from datetime import datetime
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy import text # noqa: E402
from app.database import get_session_local # noqa: E402
RELAY_URL = "http://127.0.0.1:8800/send" # 与 daily_push.py 同一中继通道(不新造通道)
SOURCE = "管理会计OS"
def current_quarter(d=None):
d = d or datetime.now()
return "%dQ%d" % (d.year, (d.month - 1) // 3 + 1)
def build_message(rows, quarter):
"""rows: list of dict(id,title,owner,progress,confidence,quarter,entity_name)"""
lines = ["## ⏰ OKR 季度复盘提醒(%s)" % quarter, ""]
lines.append("请各目标负责人更新 **进度(progress)** 与 **信心指数(confidence)**。")
lines.append("")
lines.append("| 目标 | 负责人 | 进度 | 信心 | 季度 |")
lines.append("|---|---|---|---|---|")
for r in rows:
lines.append("| %s | %s | %s%% | %s | %s |" % (
r["title"] or "-", r["owner"] or "待指定",
r["progress"] if r["progress"] is not None else "-",
r["confidence"] if r["confidence"] is not None else "-",
r["quarter"] or "未设季度"))
lines.append("")
lines.append("共 %d 个进行中目标;请在 CMA → 战略地图 / OKR 详情中更新。" % len(rows))
lines.append("")
lines.append("---")
lines.append("💡 复盘节奏:季度中期校准一次,避免期末才发现偏差。")
return "\n".join(lines)
def push_wecom(msg):
"""复用 daily_push.py 的企微中继推送"""
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:
return json.loads(resp.read().decode("utf-8"))
except Exception as e:
return {"ok": False, "error": "推送异常: %s" % e}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--push", action="store_true", help="真正推送到企微中继(默认 dry-run 只打印)")
ap.add_argument("--entity-id", type=int, default=None, help="仅指定企业")
args = ap.parse_args()
quarter = current_quarter()
db = get_session_local()()
try:
sql = ("SELECT o.id, o.title, o.owner, o.progress, o.confidence, o.quarter, o.entity_id, e.name AS entity_name "
"FROM objectives o LEFT JOIN entities e ON e.id = o.entity_id "
"WHERE o.status = 'active'")
params = {}
if args.entity_id is not None:
sql += " AND o.entity_id = :eid"
params["eid"] = args.entity_id
# 本季度 + 未设季度(历史遗留)的目标都纳入提醒
sql += " AND (o.quarter = :q OR o.quarter IS NULL OR o.quarter = '')"
params["q"] = quarter
sql += " ORDER BY o.entity_id, o.id"
rows = [dict(r._mapping) for r in db.execute(text(sql), params).fetchall()]
msg = build_message(rows, quarter)
print("== 目标数: %d(季度 %s;entity_id=%s)" % (len(rows), quarter, args.entity_id))
print(msg)
if args.push and rows:
res = push_wecom(msg)
ok = bool(res.get("ok", True)) and not res.get("error")
print("== 推送结果:", json.dumps(res, ensure_ascii=False)[:300])
status = "sent" if ok else "failed"
err = None if ok else str(res.get("error"))[:400]
else:
status, err = "pending", None
# 复用既有 notifications 机制留存审计(notification_logs)
db.execute(text(
"INSERT INTO notification_logs (channel, recipient, title, content, status, error_msg, sent_at, created_at) "
"VALUES ('wecom', :r, :t, :c, :s, :e, :sa, NOW())"),
{"r": "企微群(中继)", "t": "OKR 季度复盘提醒(%s)" % quarter, "c": msg,
"s": status, "e": err, "sa": datetime.now() if status == "sent" else None})
db.commit()
print("== notification_logs 已记录: status=%s" % status)
finally:
db.close()
if __name__ == "__main__":
main()