feat(park): 大屏设备绑定流 — 永久设备ID+连接码+园区端绑定+MQTT下发设备token

- park_screens 加 status/code/code_expires/bound_at、tenant_id 可空(alembic 0002)。
- auth 加 create/parse_device_token {tenant_id,device_id}。
- tenants 加 ensures_device/set_device_code/find_device_by_code/bind_device/unbind_device。
- 端点:/park/api/devices/register、/park/api/devices/{device_id}/code(8位码TTL10min)、
  /park/tenants/{tid}/screens/bind-by-code(校验码→绑定→MQTT opc/display/bind/<dev> 下发 token)、
  /park/api/devices/{device_id}/unbind。_resolve_tenant 支持设备 token。
- 验证(TestClient):register→code→建园区→绑码(bound/命名)→设备token取数据返回绑定园区。
This commit is contained in:
Pine
2026-08-24 20:10:46 +08:00
parent 5752b3816d
commit b6315f69b0
5 changed files with 214 additions and 8 deletions
+41
View File
@@ -0,0 +1,41 @@
"""park_screens 绑定字段(status/code/code_expires/bound_at),tenant_id 可空(幂等)
Revision ID: 0002_park_screen_bind
Revises: 0001_initial
Create Date: 2026-08-24
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
revision = "0002_park_screen_bind"
down_revision = "0001_initial"
branch_labels = None
depends_on = None
def _existing_cols(table: str) -> set[str]:
conn = op.get_bind()
rows = conn.exec_driver_sql(f"PRAGMA table_info({table})").fetchall()
return {r[1] for r in rows}
def upgrade() -> None:
cols = _existing_cols("park_screens")
spec = {
"status": sa.Column("status", sa.String(), server_default="unbound"),
"code": sa.Column("code", sa.String(), server_default=""),
"code_expires": sa.Column("code_expires", sa.String(), server_default=""),
"bound_at": sa.Column("bound_at", sa.String(), server_default=""),
}
for name, col in spec.items():
if name not in cols:
op.add_column("park_screens", col)
def downgrade() -> None:
cols = _existing_cols("park_screens")
for name in ("status", "code", "code_expires", "bound_at"):
if name in cols:
op.drop_column("park_screens", name)