"""客户生命周期判定(管理域,批次A)——距今天数口径(v2 §1.1 #3 决议)。 阈值取已确认默认(v1 §21,用户拍板):新客 7 天、沉睡 30 天;历史互动 门槛 MIN_HISTORY_MSGS=5 对齐 analytics churn real 的 min_msgs=5。 analytics 现有页(churn/客户资产)**暂不切换**到此模块——两套数字的 差异在 metric-definitions 标注(v2 §5);后续统一时再接,避免动既有 测试。 本模块为纯叶子节点,禁止 import app.storage(防止 storage→analytics 消费时成环)。 """ from __future__ import annotations import datetime as dt NEW_DAYS = 7 DORMANT_DAYS = 30 MIN_HISTORY_MSGS = 5 STATES = ("new", "active", "cooling", "dormant", "reactivated") def _to_date(day: int) -> dt.date: return dt.date(day // 10000, (day // 100) % 100, day % 100) def day_add(day: int, delta_days: int) -> int: """yyyymmdd 键加减天数。""" d = _to_date(day) + dt.timedelta(days=delta_days) return d.year * 10000 + d.month * 100 + d.day def days_between(day_a: int, day_b: int) -> int: """b − a 的天数(输入 yyyymmdd 键)。""" return (_to_date(day_b) - _to_date(day_a)).days def classify( first_day: int | None, last_day: int | None, prev_day: int | None, as_of: int, hist_msgs: int, ) -> str | None: """as_of 日的客户状态;输入均为互动日键(yyyymmdd,上海自然日)。 - first_day: 该客户全历史首次互动日;last_day: ≤as_of 的最近互动日; prev_day: last_day 之前的一次互动日(无则 None);hist_msgs: 历史总消息数。 - 返回 None = 未激活(first/last 缺)或历史不足的长期静默(不计入任何列)。 判定优先级:new → reactivated → active → cooling → dormant。 """ if first_day is None or last_day is None: return None if first_day >= day_add(as_of, -(NEW_DAYS - 1)): return "new" if ( last_day == as_of and prev_day is not None and days_between(prev_day, last_day) > DORMANT_DAYS ): return "reactivated" gap = days_between(last_day, as_of) if gap < NEW_DAYS: return "active" if gap < DORMANT_DAYS: return "cooling" if hist_msgs >= MIN_HISTORY_MSGS: return "dormant" return None