diff --git a/app/models/__init__.py b/app/models/__init__.py index 1f9a0d8..694aa00 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -7,6 +7,10 @@ from app.models.ad_watch_log import AdWatchLog # noqa: F401 from app.models.admin import AdminAuditLog, AdminUser # noqa: F401 from app.models.admin_role import AdminRole # noqa: F401 from app.models.analytics_event import AnalyticsEvent # noqa: F401 +from app.models.analytics_selfstat import ( # noqa: F401 + AnalyticsSelfStat, + AnalyticsSelfStatEvent, +) from app.models.app_config import AppConfig # noqa: F401 from app.models.comparison import ComparisonRecord # noqa: F401 from app.models.cps_activity import CpsActivity # noqa: F401 diff --git a/app/models/analytics_selfstat.py b/app/models/analytics_selfstat.py new file mode 100644 index 0000000..03bb79c --- /dev/null +++ b/app/models/analytics_selfstat.py @@ -0,0 +1,59 @@ +"""埋点/上报成功率自报计数快照表(append-only)。 + +客户端周期上报「自 epoch 起算的累计计数」;服务端只存原始快照,查询时在 Python 侧差分聚合 +(见 app/admin/repositories/analytics_health.py)。与既有 analytics_event 表完全独立。 + +- analytics_selfstat :一快照一行(快照头 + 设备维度 + 设备级诊断量) +- analytics_selfstat_event :一 event 一行(四类累计计数),外键指向快照头 +""" +from __future__ import annotations + +from datetime import datetime + +from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, String, func +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.base import Base + + +class AnalyticsSelfStat(Base): + __tablename__ = "analytics_selfstat" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + device_id: Mapped[str] = mapped_column(String(64), index=True, nullable=False) + epoch_id: Mapped[str] = mapped_column(String(64), index=True, nullable=False) + # 设备维度(每设备固定,下钻用) + app_ver: Mapped[str | None] = mapped_column(String(32), nullable=True) + oem: Mapped[str | None] = mapped_column(String(32), nullable=True) + os: Mapped[str | None] = mapped_column(String(32), nullable=True) + # 设备级诊断量(累计;queue_depth 是瞬时 gauge) + batches_attempted: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0) + batches_ok: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0) + batches_fail: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0) + retries: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0) + queue_depth: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + sent_at: Mapped[int | None] = mapped_column(BigInteger, nullable=True) # 端上报时刻 epoch ms + # 服务端接收时间(权威,用于时间分桶与分区排序) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), index=True, nullable=False + ) + + def __repr__(self) -> str: # pragma: no cover + return f"" + + +class AnalyticsSelfStatEvent(Base): + __tablename__ = "analytics_selfstat_event" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + snapshot_id: Mapped[int] = mapped_column( + ForeignKey("analytics_selfstat.id"), index=True, nullable=False + ) + event: Mapped[str] = mapped_column(String(64), index=True, nullable=False) + attempted: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0) + drop_capture: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0) + delivered: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0) + drop_undelivered: Mapped[int] = mapped_column(BigInteger, nullable=False, default=0) + + def __repr__(self) -> str: # pragma: no cover + return f""