refactor(device): device 表→device_liveness + liveness 接口只返 bool(review 意见)
- 表/类改名 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,不受影响。
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"<Device id={self.id} user_id={self.user_id} "
|
||||
f"<DeviceLiveness id={self.id} user_id={self.user_id} "
|
||||
f"device_id={self.device_id} state={self.liveness_state}>"
|
||||
)
|
||||
|
||||
+16
-16
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user