import asyncio from sqlalchemy import text from sqlalchemy.ext.asyncio import create_async_engine from database.models import Base from config import settings from utils.logger import session_logger import urllib.parse async def _ensure_manual_sleep_mode_column(conn) -> None: result = await conn.execute( text( """ SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'device_settings' AND COLUMN_NAME = 'manual_sleep_mode' """ ) ) if int(result.scalar() or 0) > 0: return await conn.execute( text( """ ALTER TABLE device_settings ADD COLUMN manual_sleep_mode TINYINT NOT NULL DEFAULT 0 AFTER sleep_mode """ ) ) async def _ensure_schedule_suppressed_until_column(conn) -> None: result = await conn.execute( text( """ SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'device_settings' AND COLUMN_NAME = 'schedule_suppressed_until' """ ) ) if int(result.scalar() or 0) > 0: return await conn.execute( text( """ ALTER TABLE device_settings ADD COLUMN schedule_suppressed_until DATETIME NULL AFTER manual_sleep_mode """ ) ) async def _ensure_columns(conn, *, table_name: str, columns: list[tuple[str, str]]) -> None: for column_name, definition in columns: result = await conn.execute( text( """ SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table_name AND COLUMN_NAME = :column_name """ ), {"table_name": table_name, "column_name": column_name}, ) if int(result.scalar() or 0) > 0: continue await conn.execute(text(f"ALTER TABLE {table_name} ADD COLUMN {definition}")) async def _ensure_device_alarm_location_columns(conn) -> None: await _ensure_columns( conn, table_name="device_alarm_events", columns=[ ("coord_type", "coord_type VARCHAR(16) NULL AFTER source_msg_id"), ("lat", "lat DECIMAL(10, 7) NULL AFTER coord_type"), ("lng", "lng DECIMAL(10, 7) NULL AFTER lat"), ("location_updated_at", "location_updated_at DATETIME NULL AFTER lng"), ("address", "address VARCHAR(255) NULL AFTER location_updated_at"), ("address_resolved_at", "address_resolved_at DATETIME NULL AFTER address"), ("address_resolve_status", "address_resolve_status TINYINT NULL AFTER address_resolved_at"), ], ) async def _ensure_device_settings_metadata_columns(conn) -> None: await _ensure_columns( conn, table_name="device_settings", columns=[ ("imei", "imei VARCHAR(64) NULL AFTER `version`"), ("device_type", "device_type TINYINT NULL AFTER imei"), ("ota_channel", "ota_channel VARCHAR(32) NULL AFTER device_type"), ], ) async def _ensure_device_firmware_update_ota_columns(conn) -> None: await _ensure_columns( conn, table_name="device_firmware_update", columns=[ ("ota_channel", "ota_channel VARCHAR(32) NULL AFTER firmware_version"), ("target_version", "target_version VARCHAR(64) NULL AFTER ota_channel"), ("firmware_url", "firmware_url TEXT NULL AFTER target_version"), ("firmware_id", "firmware_id INT NULL AFTER firmware_url"), ("source", "source VARCHAR(32) NULL AFTER firmware_id"), ], ) async def _ensure_device_ota_tables(conn) -> None: await conn.execute( text( """ CREATE TABLE IF NOT EXISTS device_ota_channels ( id INT NOT NULL AUTO_INCREMENT, channel_key VARCHAR(32) NOT NULL, name VARCHAR(64) NOT NULL, device_type TINYINT NULL, enabled TINYINT(1) NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uq_device_ota_channels_key (channel_key), KEY idx_device_ota_channels_device_type (device_type) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ ) ) await conn.execute( text( """ CREATE TABLE IF NOT EXISTS device_ota_firmwares ( id INT NOT NULL AUTO_INCREMENT, channel_key VARCHAR(32) NOT NULL, version VARCHAR(64) NOT NULL, firmware_url TEXT NOT NULL, checksum VARCHAR(128) NULL, file_size BIGINT NULL, release_status VARCHAR(32) NOT NULL DEFAULT 'draft', release_channel VARCHAR(32) NOT NULL DEFAULT 'stable', is_latest TINYINT(1) NOT NULL DEFAULT 0, notes TEXT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, published_at DATETIME NULL, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uq_device_ota_firmware_channel_version (channel_key, version), KEY idx_device_ota_firmware_latest (channel_key, release_status, is_latest) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ ) ) await conn.execute( text( """ INSERT INTO device_ota_channels (channel_key, name, device_type, enabled) VALUES ('legacy', 'Legacy devices', NULL, 1), ('no_gps', 'NFC without GPS', 1, 1), ('gps', 'NFC with GPS', 2, 1) ON DUPLICATE KEY UPDATE channel_key = channel_key """ ) ) async def _ensure_child_location_address_columns(conn) -> None: await _ensure_columns( conn, table_name="child_location_current", columns=[ ("address", "address VARCHAR(255) NULL AFTER updated_at"), ("address_resolved_at", "address_resolved_at DATETIME NULL AFTER address"), ("address_resolve_status", "address_resolve_status TINYINT NULL AFTER address_resolved_at"), ], ) async def _ensure_bind_session_card_columns(conn) -> None: await _ensure_columns( conn, table_name="device_bind_sessions", columns=[ ("bind_mode", "bind_mode TINYINT NOT NULL DEFAULT 1 AFTER status"), ("card_uuid", "card_uuid VARCHAR(64) NULL AFTER bind_mode"), ], ) result = await conn.execute( text( """ SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'device_bind_sessions' AND INDEX_NAME = 'idx_device_bind_sessions_card_uuid' """ ) ) if int(result.scalar() or 0) == 0: await conn.execute(text("ALTER TABLE device_bind_sessions ADD INDEX idx_device_bind_sessions_card_uuid (card_uuid)")) async def _ensure_cards_allow_multiple_per_device(conn) -> None: result = await conn.execute( text( """ SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'cards' AND INDEX_NAME = 'idx_cards_device_id' """ ) ) if int(result.scalar() or 0) == 0: await conn.execute(text("ALTER TABLE cards ADD INDEX idx_cards_device_id (device_id)")) result = await conn.execute( text( """ SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'cards' AND INDEX_NAME = 'uq_cards_device_id' """ ) ) if int(result.scalar() or 0) > 0: await conn.execute(text("ALTER TABLE cards DROP INDEX uq_cards_device_id")) async def _ensure_device_family_tables(conn) -> None: await conn.execute( text( """ CREATE TABLE IF NOT EXISTS device_family_members ( id BIGINT NOT NULL AUTO_INCREMENT, device_id VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, role TINYINT NOT NULL DEFAULT 2, display_name VARCHAR(64) NULL, status TINYINT NOT NULL DEFAULT 1, invited_by_user_id BIGINT NULL, joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, removed_at DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uq_device_family_member (device_id, user_id), KEY idx_device_family_members_device_status (device_id, status), KEY idx_device_family_members_user_status (user_id, status), KEY idx_device_family_members_invited_by (invited_by_user_id), CONSTRAINT fk_device_family_members_device FOREIGN KEY (device_id) REFERENCES device_auth (device_id), CONSTRAINT fk_device_family_members_user FOREIGN KEY (user_id) REFERENCES parents (user_id), CONSTRAINT fk_device_family_members_invited_by FOREIGN KEY (invited_by_user_id) REFERENCES parents (user_id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ ) ) await _ensure_columns( conn, table_name="device_family_members", columns=[ ("display_name", "display_name VARCHAR(64) NULL AFTER role"), ], ) await conn.execute( text( """ CREATE TABLE IF NOT EXISTS device_family_invitations ( id BIGINT NOT NULL AUTO_INCREMENT, invite_token CHAR(36) NOT NULL, device_id VARCHAR(64) NOT NULL, owner_user_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 1, expires_at DATETIME NOT NULL, accepted_by_user_id BIGINT NULL, accepted_at DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uq_device_family_invite_token (invite_token), KEY idx_device_family_invites_device_status (device_id, status), KEY idx_device_family_invites_owner_status (owner_user_id, status), KEY idx_device_family_invites_expires_at (expires_at), KEY idx_device_family_invites_accepted_by (accepted_by_user_id), CONSTRAINT fk_device_family_invites_device FOREIGN KEY (device_id) REFERENCES device_auth (device_id), CONSTRAINT fk_device_family_invites_owner FOREIGN KEY (owner_user_id) REFERENCES parents (user_id), CONSTRAINT fk_device_family_invites_accepted_by FOREIGN KEY (accepted_by_user_id) REFERENCES parents (user_id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ ) ) await conn.execute( text( """ INSERT INTO device_family_members ( device_id, user_id, role, status, invited_by_user_id, joined_at, created_at, updated_at ) SELECT db.device_id, db.owner_user_id, 1, 1, NULL, COALESCE(db.bound_at, CURRENT_TIMESTAMP), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP FROM device_bindings AS db LEFT JOIN device_family_members AS dfm ON dfm.device_id = db.device_id AND dfm.user_id = db.owner_user_id WHERE db.status = 1 AND dfm.id IS NULL """ ) ) async def _ensure_parent_wechat_accounts_table(conn) -> None: await conn.execute( text( """ CREATE TABLE IF NOT EXISTS parent_wechat_accounts ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, app_id VARCHAR(64) NOT NULL, account_type VARCHAR(32) NOT NULL, openid VARCHAR(64) NOT NULL, unionid VARCHAR(64) NULL, subscribed TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uq_parent_wechat_app_openid (app_id, openid), UNIQUE KEY uq_parent_wechat_user_type (user_id, account_type), KEY idx_parent_wechat_user (user_id), KEY idx_parent_wechat_unionid (unionid), CONSTRAINT fk_parent_wechat_user FOREIGN KEY (user_id) REFERENCES parents (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ ) ) async def _ensure_parent_wechat_identities_table(conn) -> None: await conn.execute( text( """ CREATE TABLE IF NOT EXISTS parent_wechat_identities ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, app_id VARCHAR(64) NOT NULL, account_type VARCHAR(32) NOT NULL, openid VARCHAR(64) NOT NULL, unionid VARCHAR(64) NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uq_parent_wechat_identity_app_openid (app_id, account_type, openid), KEY idx_parent_wechat_identity_user (user_id), KEY idx_parent_wechat_identity_unionid (unionid), CONSTRAINT fk_parent_wechat_identity_user FOREIGN KEY (user_id) REFERENCES parents (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ ) ) async def _ensure_wechat_mp_bind_states_table(conn) -> None: await conn.execute( text( """ CREATE TABLE IF NOT EXISTS wechat_mp_bind_states ( id BIGINT NOT NULL AUTO_INCREMENT, state VARCHAR(128) NOT NULL, user_id BIGINT NOT NULL, expires_at DATETIME NOT NULL, consumed_at DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uq_wechat_mp_bind_state (state), KEY idx_wechat_mp_bind_user (user_id), KEY idx_wechat_mp_bind_expires (expires_at), CONSTRAINT fk_wechat_mp_bind_user FOREIGN KEY (user_id) REFERENCES parents (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ ) ) async def init_db(): """初始化数据库,创建所有表""" try: encoded_password = urllib.parse.quote_plus(settings.db_password) connection_string = f"mysql+aiomysql://{settings.db_user}:{encoded_password}@{settings.db_host}:{settings.db_port}/{settings.db_name}" engine = create_async_engine( connection_string, echo=settings.db_echo ) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) await _ensure_manual_sleep_mode_column(conn) await _ensure_schedule_suppressed_until_column(conn) await _ensure_device_settings_metadata_columns(conn) await _ensure_device_firmware_update_ota_columns(conn) await _ensure_device_ota_tables(conn) await _ensure_device_alarm_location_columns(conn) await _ensure_child_location_address_columns(conn) await _ensure_bind_session_card_columns(conn) await _ensure_cards_allow_multiple_per_device(conn) await _ensure_device_family_tables(conn) await _ensure_parent_wechat_accounts_table(conn) await _ensure_parent_wechat_identities_table(conn) await _ensure_wechat_mp_bind_states_table(conn) await engine.dispose() session_logger.info("system", "database", "数据库表已成功创建") return True except Exception as e: session_logger.error("system", "database", f"创建数据库表失败: {str(e)}") return False