mirror of
https://github.com/This-weiyuqing/mi-fitness-health.git
synced 2026-10-18 03:03:49 +08:00
chore: localize CLI output and comments to Chinese
This commit is contained in:
1 parent
636d2857c3
commit
ebc94b50e9
5 files changed
+70
-71
No files matched your search
@@ -244,11 +244,10 @@ class MiFitnessCloudAdapter(DataAdapter):
|
|||||||
return preferred_region
|
return preferred_region
|
||||||
|
|
||||||
async def _discover_data_types(self) -> list[str]:
|
async def _discover_data_types(self) -> list[str]:
|
||||||
# The cloud API does not provide a reliable capability endpoint. The
|
# 小米健康云没有可靠的能力发现接口。旧实现用固定历史区间探测,
|
||||||
# previous implementation probed a fixed historical range, which made
|
# 当该区间无数据时会导致自动同步静默跳过大部分指标。
|
||||||
# automatic sync silently skip most metrics when that range had no data.
|
# 这里直接返回本适配器支持的全部数据类型;某类型无记录时同步 0 条即可,
|
||||||
# Return all data types supported by this adapter; syncing a type with no
|
# 比漏掉近期数据更安全。
|
||||||
# records is safe and preferable to missing recent data.
|
|
||||||
return [
|
return [
|
||||||
"daily_activity",
|
"daily_activity",
|
||||||
"heart_rate",
|
"heart_rate",
|
||||||
|
|||||||
+45
-45
@@ -26,104 +26,104 @@ async def _check_adapter_health(adapter) -> tuple[bool, list[str], str | None]:
|
|||||||
def cmd_setup(args):
|
def cmd_setup(args):
|
||||||
if args.mode == "mi_fitness_cloud" and args.user_id and args.pass_token:
|
if args.mode == "mi_fitness_cloud" and args.user_id and args.pass_token:
|
||||||
save_mi_fitness_token(args.user_id, args.pass_token)
|
save_mi_fitness_token(args.user_id, args.pass_token)
|
||||||
config = Config(mode="mi_fitness_cloud", region=args.region or "ru")
|
config = Config(mode="mi_fitness_cloud", region=args.region or "cn")
|
||||||
save_config(config)
|
save_config(config)
|
||||||
print("✅ Конфигурация сохранена (mi_fitness_cloud)")
|
print("✅ 配置已保存(mi_fitness_cloud)")
|
||||||
print(f" User ID: {args.user_id}")
|
print(f" User ID: {args.user_id}")
|
||||||
print(f" Region: {config.region}")
|
print(f" Region: {config.region}")
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"{PROGRAM_NAME} - Setup Wizard")
|
print(f"{PROGRAM_NAME} - 配置向导")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print()
|
print()
|
||||||
user_id = input("Mi Fitness user_id: ").strip()
|
user_id = input("Mi Fitness user_id: ").strip()
|
||||||
pass_token = input("Mi Fitness passToken: ").strip()
|
pass_token = input("Mi Fitness passToken: ").strip()
|
||||||
region = input("Region [ru]: ").strip() or "ru"
|
region = input("区域 [cn]: ").strip() or "cn"
|
||||||
if not user_id or not pass_token:
|
if not user_id or not pass_token:
|
||||||
print("❌ Требуются user_id и passToken")
|
print("❌ 必须提供 user_id 和 passToken")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
save_mi_fitness_token(user_id, pass_token)
|
save_mi_fitness_token(user_id, pass_token)
|
||||||
config = Config(mode="mi_fitness_cloud", region=region)
|
config = Config(mode="mi_fitness_cloud", region=region)
|
||||||
save_config(config)
|
save_config(config)
|
||||||
print("✅ Mi Fitness конфигурация сохранена!")
|
print("✅ Mi Fitness 配置已保存!")
|
||||||
|
|
||||||
|
|
||||||
def cmd_doctor(args):
|
def cmd_doctor(args):
|
||||||
print(f"{PROGRAM_NAME} - Doctor")
|
print(f"{PROGRAM_NAME} - 诊断")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print()
|
print()
|
||||||
config_path = get_config_path()
|
config_path = get_config_path()
|
||||||
print(f"Конфигурация: {config_path}")
|
print(f"配置文件: {config_path}")
|
||||||
|
|
||||||
if not config_path.exists():
|
if not config_path.exists():
|
||||||
print("❌ Конфигурация не найдена")
|
print("❌ 未找到配置文件")
|
||||||
print(f" Запустите: {PROGRAM_NAME} setup")
|
print(f" 请运行: {PROGRAM_NAME} setup")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = load_config()
|
config = load_config()
|
||||||
print("✅ Конфигурация загружена")
|
print("✅ 配置已加载")
|
||||||
print(f" Режим: {config.mode}")
|
print(f" 模式: {config.mode}")
|
||||||
user_id, pass_token = load_mi_fitness_token()
|
user_id, pass_token = load_mi_fitness_token()
|
||||||
if user_id and pass_token:
|
if user_id and pass_token:
|
||||||
print("✅ Mi Fitness credentials найдены")
|
print("✅ 已找到 Mi Fitness 凭据")
|
||||||
print(f" User ID: {user_id}")
|
print(f" User ID: {user_id}")
|
||||||
print(f" Region: {config.region}")
|
print(f" Region: {config.region}")
|
||||||
adapter = MiFitnessCloudAdapter(
|
adapter = MiFitnessCloudAdapter(
|
||||||
user_id=user_id, pass_token=pass_token, region=config.region
|
user_id=user_id, pass_token=pass_token, region=config.region
|
||||||
)
|
)
|
||||||
connected, data_types, region = asyncio.run(_check_adapter_health(adapter))
|
connected, data_types, region = asyncio.run(_check_adapter_health(adapter))
|
||||||
print(f" Подключение: {'✅' if connected else '❌'}")
|
print(f" 连接状态: {'✅' if connected else '❌'}")
|
||||||
if connected:
|
if connected:
|
||||||
print(f" Определённый регион: {region}")
|
print(f" 识别到的区域: {region}")
|
||||||
print(f" Типы данных: {', '.join(data_types)}")
|
print(f" 数据类型: {', '.join(data_types)}")
|
||||||
else:
|
else:
|
||||||
print("❌ Mi Fitness credentials не найдены")
|
print("❌ 未找到 Mi Fitness 凭据")
|
||||||
print(f" Запустите: {PROGRAM_NAME} setup")
|
print(f" 请运行: {PROGRAM_NAME} setup")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print(f"База данных: {config.database_path}")
|
print(f"数据库: {config.database_path}")
|
||||||
if config.database_path.exists():
|
if config.database_path.exists():
|
||||||
Database(config.database_path)
|
Database(config.database_path)
|
||||||
print("✅ База данных доступна")
|
print("✅ 数据库可用")
|
||||||
else:
|
else:
|
||||||
print("ℹ️ База данных будет создана при первом запуске")
|
print("ℹ️ 数据库将在首次运行时创建")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка загрузки конфигурации: {e}")
|
print(f"❌ 加载配置失败: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
async def cmd_sync_async(args):
|
async def cmd_sync_async(args):
|
||||||
print(f"{PROGRAM_NAME} - Sync")
|
print(f"{PROGRAM_NAME} - 同步")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print()
|
print()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = load_config()
|
config = load_config()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка загрузки конфигурации: {e}")
|
print(f"❌ 加载配置失败: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if config.mode == "not_configured":
|
if config.mode == "not_configured":
|
||||||
print("❌ Сервер не настроен")
|
print("❌ 服务尚未配置")
|
||||||
print(f" Запустите: {PROGRAM_NAME} setup")
|
print(f" 请运行: {PROGRAM_NAME} setup")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
user_id, pass_token = load_mi_fitness_token()
|
user_id, pass_token = load_mi_fitness_token()
|
||||||
if not user_id or not pass_token:
|
if not user_id or not pass_token:
|
||||||
print("❌ Mi Fitness credentials не найдены")
|
print("❌ 未找到 Mi Fitness 凭据")
|
||||||
print(f" Запустите: {PROGRAM_NAME} setup")
|
print(f" 请运行: {PROGRAM_NAME} setup")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
db = Database(config.database_path)
|
db = Database(config.database_path)
|
||||||
adapter = MiFitnessCloudAdapter(user_id=user_id, pass_token=pass_token, region=config.region)
|
adapter = MiFitnessCloudAdapter(user_id=user_id, pass_token=pass_token, region=config.region)
|
||||||
if not await adapter.connect():
|
if not await adapter.connect():
|
||||||
print("❌ Не удалось подключиться к Mi Fitness API")
|
print("❌ 无法连接到 Mi Fitness API")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
sync_service = SyncService(adapter, db)
|
sync_service = SyncService(adapter, db)
|
||||||
data_types = [args.type] if args.type else adapter.get_available_data_types()
|
data_types = [args.type] if args.type else adapter.get_available_data_types()
|
||||||
print(f"Синхронизация {len(data_types)} типов данных...")
|
print(f"正在同步 {len(data_types)} 种数据类型...")
|
||||||
print()
|
print()
|
||||||
for data_type in data_types:
|
for data_type in data_types:
|
||||||
try:
|
try:
|
||||||
@@ -132,12 +132,12 @@ async def cmd_sync_async(args):
|
|||||||
start_date=args.start_date,
|
start_date=args.start_date,
|
||||||
end_date=args.end_date,
|
end_date=args.end_date,
|
||||||
)
|
)
|
||||||
print(f"✅ {data_type}: {result['added']} добавлено, {result['updated']} обновлено")
|
print(f"✅ {data_type}: 新增 {result['added']} 条,更新 {result['updated']} 条")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ {data_type}: {e}")
|
print(f"❌ {data_type}: {e}")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("Синхронизация завершена!")
|
print("同步完成!")
|
||||||
|
|
||||||
|
|
||||||
def cmd_sync(args):
|
def cmd_sync(args):
|
||||||
@@ -146,20 +146,20 @@ def cmd_sync(args):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog=PROGRAM_NAME, description="MCP server for Mi Fitness data"
|
prog=PROGRAM_NAME, description="小米运动健康数据 MCP Server"
|
||||||
)
|
)
|
||||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
subparsers = parser.add_subparsers(dest="command", help="可用命令")
|
||||||
subparsers.add_parser("serve", help="Run MCP server")
|
subparsers.add_parser("serve", help="运行 MCP Server")
|
||||||
|
|
||||||
setup_parser = subparsers.add_parser("setup", help="Configure the server")
|
setup_parser = subparsers.add_parser("setup", help="配置服务")
|
||||||
setup_parser.add_argument("--mode", choices=["mi_fitness_cloud"], help="Setup mode")
|
setup_parser.add_argument("--mode", choices=["mi_fitness_cloud"], help="配置模式")
|
||||||
setup_parser.add_argument("--user-id", help="Mi Fitness user ID")
|
setup_parser.add_argument("--user-id", help="Mi Fitness user ID")
|
||||||
setup_parser.add_argument("--pass-token", help="Mi Fitness passToken")
|
setup_parser.add_argument("--pass-token", help="Mi Fitness passToken")
|
||||||
setup_parser.add_argument("--region", help="Cloud region")
|
setup_parser.add_argument("--region", help="云端区域")
|
||||||
|
|
||||||
subparsers.add_parser("doctor", help="Check configuration and diagnose issues")
|
subparsers.add_parser("doctor", help="检查配置并诊断问题")
|
||||||
|
|
||||||
sync_parser = subparsers.add_parser("sync", help="Sync data from source")
|
sync_parser = subparsers.add_parser("sync", help="从数据源同步数据")
|
||||||
sync_parser.add_argument(
|
sync_parser.add_argument(
|
||||||
"--type",
|
"--type",
|
||||||
choices=[
|
choices=[
|
||||||
@@ -172,10 +172,10 @@ def main():
|
|||||||
"stress",
|
"stress",
|
||||||
"abnormal_heart_beat",
|
"abnormal_heart_beat",
|
||||||
],
|
],
|
||||||
help="Type of data to sync",
|
help="要同步的数据类型",
|
||||||
)
|
)
|
||||||
sync_parser.add_argument("--start-date", help="Start date (YYYY-MM-DD)")
|
sync_parser.add_argument("--start-date", help="开始日期(YYYY-MM-DD)")
|
||||||
sync_parser.add_argument("--end-date", help="End date (YYYY-MM-DD)")
|
sync_parser.add_argument("--end-date", help="结束日期(YYYY-MM-DD)")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.command == "serve" or args.command is None:
|
if args.command == "serve" or args.command is None:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class QueryService:
|
|||||||
"""Get daily activity summaries for date range."""
|
"""Get daily activity summaries for date range."""
|
||||||
records = self.db.query_daily_activity(self.user_id, start_date, end_date)
|
records = self.db.query_daily_activity(self.user_id, start_date, end_date)
|
||||||
|
|
||||||
# Group by date and aggregate
|
# 按日期分组并聚合
|
||||||
summaries = {}
|
summaries = {}
|
||||||
for record in records:
|
for record in records:
|
||||||
date = record["date"]
|
date = record["date"]
|
||||||
@@ -73,7 +73,7 @@ class QueryService:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Apply aggregation if needed
|
# 按需执行周/月聚合
|
||||||
if granularity == "week":
|
if granularity == "week":
|
||||||
series = self._aggregate_by_week(series, aggregation)
|
series = self._aggregate_by_week(series, aggregation)
|
||||||
elif granularity == "month":
|
elif granularity == "month":
|
||||||
@@ -91,7 +91,7 @@ class QueryService:
|
|||||||
|
|
||||||
for item in series:
|
for item in series:
|
||||||
date = datetime.strptime(item["date"], "%Y-%m-%d")
|
date = datetime.strptime(item["date"], "%Y-%m-%d")
|
||||||
# Get week start (Monday)
|
# 计算周起始日(周一)
|
||||||
week_start = date - timedelta(days=date.weekday())
|
week_start = date - timedelta(days=date.weekday())
|
||||||
week_key = week_start.strftime("%Y-%m-%d")
|
week_key = week_start.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ class QueryService:
|
|||||||
|
|
||||||
sessions = []
|
sessions = []
|
||||||
for record in records:
|
for record in records:
|
||||||
# Skip naps if not included
|
# 不包含小睡时跳过 nap 记录
|
||||||
if not include_naps and record.get("is_nap"):
|
if not include_naps and record.get("is_nap"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ class QueryService:
|
|||||||
"is_nap": record.get("is_nap", False),
|
"is_nap": record.get("is_nap", False),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parse stages if available
|
# 如有睡眠阶段信息则解析
|
||||||
if record.get("stages"):
|
if record.get("stages"):
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ class QueryService:
|
|||||||
|
|
||||||
workouts = []
|
workouts = []
|
||||||
for record in records:
|
for record in records:
|
||||||
# Apply filters
|
# 应用过滤条件
|
||||||
if activity_types and record["activity_type"].lower() not in [
|
if activity_types and record["activity_type"].lower() not in [
|
||||||
t.lower() for t in activity_types
|
t.lower() for t in activity_types
|
||||||
]:
|
]:
|
||||||
@@ -248,7 +248,7 @@ class QueryService:
|
|||||||
"weight_kg": record["weight_kg"],
|
"weight_kg": record["weight_kg"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add optional metrics
|
# 添加可选指标
|
||||||
optional_fields = [
|
optional_fields = [
|
||||||
"bmi",
|
"bmi",
|
||||||
"body_fat_pct",
|
"body_fat_pct",
|
||||||
@@ -264,7 +264,7 @@ class QueryService:
|
|||||||
if record.get(field) is not None:
|
if record.get(field) is not None:
|
||||||
measurement[field] = record[field]
|
measurement[field] = record[field]
|
||||||
|
|
||||||
# Filter metrics if specified
|
# 如指定指标列表则过滤字段
|
||||||
if metrics:
|
if metrics:
|
||||||
filtered = {"timestamp": measurement["timestamp"]}
|
filtered = {"timestamp": measurement["timestamp"]}
|
||||||
for metric in metrics:
|
for metric in metrics:
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class SyncService:
|
|||||||
if not self.adapter.is_connected():
|
if not self.adapter.is_connected():
|
||||||
raise RuntimeError("Adapter not connected")
|
raise RuntimeError("Adapter not connected")
|
||||||
|
|
||||||
# Get last sync state for incremental sync
|
# 获取上次同步状态,用于增量同步
|
||||||
last_record_ts = None
|
last_record_ts = None
|
||||||
if not force_full:
|
if not force_full:
|
||||||
state = self.db.get_sync_state(data_type)
|
state = self.db.get_sync_state(data_type)
|
||||||
@@ -72,7 +72,7 @@ class SyncService:
|
|||||||
skipped = 0
|
skipped = 0
|
||||||
last_ts = None
|
last_ts = None
|
||||||
|
|
||||||
# Sync based on data type
|
# 按数据类型执行同步
|
||||||
if data_type == "daily_activity":
|
if data_type == "daily_activity":
|
||||||
records = self.adapter.iter_daily_activity(start_date, end_date)
|
records = self.adapter.iter_daily_activity(start_date, end_date)
|
||||||
async for activity in self._iterate_records(records):
|
async for activity in self._iterate_records(records):
|
||||||
@@ -156,7 +156,7 @@ class SyncService:
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown data type: {data_type}")
|
raise ValueError(f"Unknown data type: {data_type}")
|
||||||
|
|
||||||
# Update sync state
|
# 更新同步状态
|
||||||
if last_ts:
|
if last_ts:
|
||||||
self.db.update_sync_state(data_type, last_ts)
|
self.db.update_sync_state(data_type, last_ts)
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class Database:
|
|||||||
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
# Daily activity table
|
# 日常活动表
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS daily_activity (
|
CREATE TABLE IF NOT EXISTS daily_activity (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@@ -60,7 +60,7 @@ class Database:
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Sleep sessions table
|
# 睡眠记录表
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS sleep_sessions (
|
CREATE TABLE IF NOT EXISTS sleep_sessions (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@@ -86,7 +86,7 @@ class Database:
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Workouts table
|
# 运动记录表
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS workouts (
|
CREATE TABLE IF NOT EXISTS workouts (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@@ -115,7 +115,7 @@ class Database:
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Body measurements table
|
# 身体测量表
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS body_measurements (
|
CREATE TABLE IF NOT EXISTS body_measurements (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@@ -218,7 +218,7 @@ class Database:
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Sync state table
|
# 同步状态表
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS sync_state (
|
CREATE TABLE IF NOT EXISTS sync_state (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -229,7 +229,7 @@ class Database:
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Create indexes for better query performance
|
# 创建索引以提升查询性能
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
CREATE INDEX IF NOT EXISTS idx_activity_user_date
|
CREATE INDEX IF NOT EXISTS idx_activity_user_date
|
||||||
ON daily_activity(user_id, date)
|
ON daily_activity(user_id, date)
|
||||||
@@ -758,7 +758,7 @@ class Database:
|
|||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
# Activity coverage
|
# 日常活动覆盖范围
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
@@ -778,7 +778,7 @@ class Database:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sleep coverage
|
# 睡眠覆盖范围
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
@@ -798,7 +798,7 @@ class Database:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Workouts coverage
|
# 运动记录覆盖范围
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
@@ -818,7 +818,7 @@ class Database:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Body measurements coverage
|
# 身体测量覆盖范围
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
|
|||||||
Reference in new issue
Block a user