From 0964d81898d890e615d142a85bf906c3bb491d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E4=B8=96=E7=9D=BF?= <2839904623@qq.com> Date: Tue, 23 Jun 2026 10:20:02 +0800 Subject: [PATCH] =?UTF-8?q?refactor(device):=20device=20=E8=A1=A8=E2=86=92?= =?UTF-8?q?device=5Fliveness=20+=20liveness=20=E6=8E=A5=E5=8F=A3=E5=8F=AA?= =?UTF-8?q?=E8=BF=94=20bool(review=20=E6=84=8F=E8=A7=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 表/类改名 device→device_liveness / Device→DeviceLiveness:该表记的是无障碍存活监控状态 (心跳/liveness_state/kill_alert_pending/推送目标),非设备信息(品牌/型号),原名误导。同步改 建表迁移表名/索引/约束(未部署→直接改建表迁移,非加 rename);API 路径 /api/v1/device 不变。 - GET /device/liveness 的 LivenessOut 只返 kill_alert_pending 一个布尔(原返回 liveness_state/ ever_protected/last_heartbeat_at 等设备详情);前端本就只读 kill_alert_pending,不受影响。 --- alembic/versions/device_kill_alert_pending.py | 4 +-- alembic/versions/device_table.py | 22 ++++++------- app/models/__init__.py | 2 +- app/models/device.py | 10 +++--- app/repositories/device.py | 32 +++++++++---------- app/schemas/device.py | 6 ++-- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/alembic/versions/device_kill_alert_pending.py b/alembic/versions/device_kill_alert_pending.py index b9eb475..b25b219 100644 --- a/alembic/versions/device_kill_alert_pending.py +++ b/alembic/versions/device_kill_alert_pending.py @@ -23,7 +23,7 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # server_default=sa.false() 跨方言: SQLite 渲染 0 / PG 渲染 false(别用 sa.text("0"),PG 布尔严格会报错), # 给存量行回填 False。 - with op.batch_alter_table('device', schema=None) as batch_op: + with op.batch_alter_table('device_liveness', schema=None) as batch_op: batch_op.add_column( sa.Column( 'kill_alert_pending', @@ -35,5 +35,5 @@ def upgrade() -> None: def downgrade() -> None: - with op.batch_alter_table('device', schema=None) as batch_op: + with op.batch_alter_table('device_liveness', schema=None) as batch_op: batch_op.drop_column('kill_alert_pending') diff --git a/alembic/versions/device_table.py b/alembic/versions/device_table.py index 194a9a0..ffe1e71 100644 --- a/alembic/versions/device_table.py +++ b/alembic/versions/device_table.py @@ -20,7 +20,7 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.create_table( - 'device', + 'device_liveness', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('device_id', sa.String(length=128), nullable=False), @@ -36,18 +36,18 @@ def upgrade() -> None: sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('user_id', 'device_id', name='uq_device_user_device'), + sa.UniqueConstraint('user_id', 'device_id', name='uq_device_liveness_user_device'), ) - with op.batch_alter_table('device', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_device_user_id'), ['user_id'], unique=False) - batch_op.create_index(batch_op.f('ix_device_device_id'), ['device_id'], unique=False) - batch_op.create_index(batch_op.f('ix_device_last_heartbeat_at'), ['last_heartbeat_at'], unique=False) + with op.batch_alter_table('device_liveness', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_device_liveness_user_id'), ['user_id'], unique=False) + batch_op.create_index(batch_op.f('ix_device_liveness_device_id'), ['device_id'], unique=False) + batch_op.create_index(batch_op.f('ix_device_liveness_last_heartbeat_at'), ['last_heartbeat_at'], unique=False) def downgrade() -> None: - with op.batch_alter_table('device', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_device_last_heartbeat_at')) - batch_op.drop_index(batch_op.f('ix_device_device_id')) - batch_op.drop_index(batch_op.f('ix_device_user_id')) + with op.batch_alter_table('device_liveness', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_device_liveness_last_heartbeat_at')) + batch_op.drop_index(batch_op.f('ix_device_liveness_device_id')) + batch_op.drop_index(batch_op.f('ix_device_liveness_user_id')) - op.drop_table('device') + op.drop_table('device_liveness') diff --git a/app/models/__init__.py b/app/models/__init__.py index f3491f1..09ce54a 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -12,7 +12,7 @@ from app.models.cps_link import CpsClick, CpsLink # noqa: F401 from app.models.cps_order import CpsOrder # noqa: F401 from app.models.cps_wx_user import CpsWxUser # noqa: F401 from app.models.comparison_milestone import ComparisonMilestoneClaim # noqa: F401 -from app.models.device import Device # noqa: F401 +from app.models.device import DeviceLiveness # noqa: F401 from app.models.coupon_state import ( # noqa: F401 CouponClaimRecord, CouponDailyCompletion, diff --git a/app/models/device.py b/app/models/device.py index 290bc9f..339d6d9 100644 --- a/app/models/device.py +++ b/app/models/device.py @@ -28,10 +28,12 @@ from sqlalchemy.orm import Mapped, mapped_column from app.db.base import Base -class Device(Base): - __tablename__ = "device" +class DeviceLiveness(Base): + # 表名不叫 device:device 易被当成「设备信息(品牌/型号/系统)」表;本表实为**无障碍存活监控状态** + # (心跳 last_heartbeat_at + liveness_state + kill_alert_pending + 推送目标 registration_id),故名 device_liveness。 + __tablename__ = "device_liveness" __table_args__ = ( - UniqueConstraint("user_id", "device_id", name="uq_device_user_device"), + UniqueConstraint("user_id", "device_id", name="uq_device_liveness_user_device"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) @@ -84,6 +86,6 @@ class Device(Base): def __repr__(self) -> str: # pragma: no cover return ( - f"" ) diff --git a/app/repositories/device.py b/app/repositories/device.py index b1c40d8..e24fe35 100644 --- a/app/repositories/device.py +++ b/app/repositories/device.py @@ -6,12 +6,12 @@ from datetime import datetime, timedelta, timezone from sqlalchemy import select from sqlalchemy.orm import Session -from app.models.device import Device +from app.models.device import DeviceLiveness -def _get(db: Session, *, user_id: int, device_id: str) -> Device | None: - stmt = select(Device).where( - Device.user_id == user_id, Device.device_id == device_id +def _get(db: Session, *, user_id: int, device_id: str) -> DeviceLiveness | None: + stmt = select(DeviceLiveness).where( + DeviceLiveness.user_id == user_id, DeviceLiveness.device_id == device_id ) return db.execute(stmt).scalar_one_or_none() @@ -24,11 +24,11 @@ def register_or_update( registration_id: str | None, platform: str = "android", app_version: str | None = None, -) -> Device: +) -> DeviceLiveness: """注册设备或更新其 registration_id / 元信息。upsert by (user_id, device_id)。""" device = _get(db, user_id=user_id, device_id=device_id) if device is None: - device = Device( + device = DeviceLiveness( user_id=user_id, device_id=device_id, registration_id=registration_id, @@ -55,7 +55,7 @@ def touch_heartbeat( device_id: str, accessibility_enabled: bool, registration_id: str | None, -) -> Device: +) -> DeviceLiveness: """处理一次心跳(心跳也能自注册)。 service 心跳或 accessibility_enabled=true 时,刷新存活并把状态机重置回 alive、 @@ -64,7 +64,7 @@ def touch_heartbeat( now = datetime.now(timezone.utc) device = _get(db, user_id=user_id, device_id=device_id) if device is None: - device = Device(user_id=user_id, device_id=device_id) + device = DeviceLiveness(user_id=user_id, device_id=device_id) db.add(device) if registration_id: @@ -82,17 +82,17 @@ def touch_heartbeat( return device -def list_overdue(db: Session, *, timeout_minutes: int) -> list[Device]: +def list_overdue(db: Session, *, timeout_minutes: int) -> list[DeviceLiveness]: """掉线设备:曾经保护过、当前 alive、心跳超时。 本期只做终端打印检测、不推送 → 不再要求有 registration_id(没接极光 token 的设备也要检出)。 """ cutoff = datetime.now(timezone.utc) - timedelta(minutes=timeout_minutes) - stmt = select(Device).where( - Device.ever_protected.is_(True), - Device.liveness_state == "alive", - Device.last_heartbeat_at.is_not(None), - Device.last_heartbeat_at < cutoff, + stmt = select(DeviceLiveness).where( + DeviceLiveness.ever_protected.is_(True), + DeviceLiveness.liveness_state == "alive", + DeviceLiveness.last_heartbeat_at.is_not(None), + DeviceLiveness.last_heartbeat_at < cutoff, ) return list(db.execute(stmt).scalars().all()) @@ -103,7 +103,7 @@ def mark_notified(db: Session, *, device_id_pk: int) -> None: kill_alert_pending 与 state 解耦(见 spec accessibility-liveness-pull-prompt.md): 心跳恢复(touch_heartbeat)只重置 state、不动此标记,故客户端下次进 App 必能拉到这次掉线。 """ - device = db.get(Device, device_id_pk) + device = db.get(DeviceLiveness, device_id_pk) if device is not None: device.liveness_state = "notified" device.notified_at = datetime.now(timezone.utc) @@ -111,7 +111,7 @@ def mark_notified(db: Session, *, device_id_pk: int) -> None: db.commit() -def get_device(db: Session, *, user_id: int, device_id: str) -> Device | None: +def get_device(db: Session, *, user_id: int, device_id: str) -> DeviceLiveness | None: """按 (user_id, device_id) 取设备(liveness 查询用)。""" return _get(db, user_id=user_id, device_id=device_id) diff --git a/app/schemas/device.py b/app/schemas/device.py index 3254e06..dd7a7c4 100644 --- a/app/schemas/device.py +++ b/app/schemas/device.py @@ -37,13 +37,11 @@ class OkResponse(BaseModel): class LivenessOut(BaseModel): - """本机掉线告警状态(后置检测 pull 版)。设备从未注册过 → 全默认(= 无告警)。""" + """本机掉线告警状态(后置检测 pull 版)。客户端只需这一个布尔判断要不要弹「开启自启动」引导, + 故只返回 kill_alert_pending(不暴露设备详情 / 内部 liveness_state 等)。从未注册过 → 默认 False(无告警)。""" model_config = ConfigDict(from_attributes=True) kill_alert_pending: bool = False - liveness_state: str = "unknown" - ever_protected: bool = False - last_heartbeat_at: datetime | None = None class LivenessAckRequest(BaseModel):