From 7802aeb16319781f5db4998e1b005962e2601077 Mon Sep 17 00:00:00 2001 From: guke Date: Thu, 16 Jul 2026 18:47:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(welfare):=20=E5=8F=AF=E6=8F=92=E6=8B=94?= =?UTF-8?q?=E4=B8=8D=E6=B4=BB=E8=B7=83=E9=A2=84=E8=AD=A6=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E5=99=A8=20+=20LogNotifier=20=E5=8D=A0=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- app/integrations/notifier.py | 43 ++++++++++++++++++++++++++++++++++ tests/test_inactivity_reset.py | 10 ++++++++ 2 files changed, 53 insertions(+) create mode 100644 app/integrations/notifier.py diff --git a/app/integrations/notifier.py b/app/integrations/notifier.py new file mode 100644 index 0000000..e1b10fe --- /dev/null +++ b/app/integrations/notifier.py @@ -0,0 +1,43 @@ +"""不活跃预警通知器(可插拔)。 + +v1 仅日志占位(LogNotifier):现状无真实推送能力(极光只用于一键登录解密 + 设备心跳告警, +心跳 worker 也只打印),先把清零主流程 + 审计做扎实。后续实现同协议的 JPushNotifier / +SmsNotifier 即可替换,worker/repo 不改。 +""" +from __future__ import annotations + +import logging +from typing import Protocol + +logger = logging.getLogger("shagua.inactivity") + + +class InactivityNotifier(Protocol): + channel: str + + def warn(self, *, user_id: int, coin: int, cash_cents: int, invite_cash_cents: int, + stage: int, days_until_reset: int) -> str: + """发预警,返回状态:'sent' / 'failed' / 'placeholder'。""" + ... + + +class LogNotifier: + """占位实现:只打印,不真推。参照 heartbeat_monitor_worker「本期先不接推送」先例。""" + + channel = "log" + + def warn(self, *, user_id: int, coin: int, cash_cents: int, invite_cash_cents: int, + stage: int, days_until_reset: int) -> str: + logger.warning( + "[inactivity-warn] user=%s coin=%s cash_cents=%s invite_cash_cents=%s " + "stage=T-%s days_until_reset=%s", + user_id, coin, cash_cents, invite_cash_cents, stage, days_until_reset, + ) + return "placeholder" + + +def get_notifier(channel: str) -> InactivityNotifier: + """按配置返回通知器。未实现的通道(jpush/sms)暂回退 LogNotifier 占位。""" + # 后续:if channel == "jpush": return JPushNotifier() + # if channel == "sms": return SmsNotifier() + return LogNotifier() diff --git a/tests/test_inactivity_reset.py b/tests/test_inactivity_reset.py index 881c3ea..b3f328a 100644 --- a/tests/test_inactivity_reset.py +++ b/tests/test_inactivity_reset.py @@ -117,3 +117,13 @@ def test_inactivity_warn_stages_parsing() -> None: assert s2.inactivity_warn_stages == [] # 空=不推 s3 = Settings(INACTIVITY_WARN_DAYS_BEFORE="2,20,7,2", INACTIVITY_RESET_DAYS=15) assert s3.inactivity_warn_stages == [7, 2] # 去重 + 丢弃 >=RESET_DAYS(20) + + +def test_log_notifier_returns_placeholder(caplog) -> None: + from app.integrations.notifier import LogNotifier, get_notifier + n = get_notifier("log") + assert isinstance(n, LogNotifier) and n.channel == "log" + status = n.warn(user_id=1, coin=10, cash_cents=20, invite_cash_cents=30, stage=7, days_until_reset=8) + assert status == "placeholder" + # 未实现通道回退 LogNotifier(占位) + assert get_notifier("jpush").channel == "log"