小程序后端接入 talkingq 共享库并新增消息与定位能力
This commit is contained in:
264
database/migrate_to_shared_talkingq.py
Normal file
264
database/migrate_to_shared_talkingq.py
Normal file
@@ -0,0 +1,264 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pymysql
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parent
|
||||
DEFAULT_SCHEMA_PATH = ROOT_DIR / "talkingq_shared_schema.sql"
|
||||
DEFAULT_ENV_PATH = ROOT_DIR.parent / "mini-program" / ".env"
|
||||
DEFAULT_TARGET_DB = "talkingq"
|
||||
DEFAULT_LEGACY_DB = "mini_program"
|
||||
|
||||
TALKINGQ_URL_TABLES = [
|
||||
"device_auth",
|
||||
"device_configs",
|
||||
"conversation_histories",
|
||||
"conversation_messages",
|
||||
"roles",
|
||||
"role_languages",
|
||||
"device_firmware_update",
|
||||
"system_config",
|
||||
]
|
||||
|
||||
MINI_PROGRAM_TABLES = [
|
||||
"parents",
|
||||
"children",
|
||||
"parent_child_relations",
|
||||
"device_bindings",
|
||||
"device_bind_sessions",
|
||||
"device_bind_history",
|
||||
"cards",
|
||||
"device_settings",
|
||||
"im_conversations",
|
||||
"im_messages",
|
||||
"child_location_current",
|
||||
"child_location_history",
|
||||
]
|
||||
|
||||
|
||||
def _env_first(*keys: str, default: str) -> str:
|
||||
for key in keys:
|
||||
value = os.getenv(key)
|
||||
if value:
|
||||
return value
|
||||
return default
|
||||
|
||||
|
||||
def load_env_file(path: Path) -> dict[str, str]:
|
||||
if not path.exists():
|
||||
return {}
|
||||
|
||||
env: dict[str, str] = {}
|
||||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
|
||||
value = value[1:-1]
|
||||
env[key] = value
|
||||
return env
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Apply the shared Banban schema to the single source-of-truth database. "
|
||||
"Existing talkingq-url data in the target database is preserved; legacy "
|
||||
"mini_program data is ignored."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"--env-file",
|
||||
default=str(DEFAULT_ENV_PATH),
|
||||
help="Optional env file used as fallback for DB_HOST/DB_PORT/DB_USER/DB_PASSWORD.",
|
||||
)
|
||||
parser.add_argument("--host")
|
||||
parser.add_argument("--port", type=int)
|
||||
parser.add_argument("--user")
|
||||
parser.add_argument("--password")
|
||||
parser.add_argument("--target-db")
|
||||
parser.add_argument("--legacy-db")
|
||||
parser.add_argument("--schema", default=str(DEFAULT_SCHEMA_PATH))
|
||||
parser.add_argument(
|
||||
"--drop-legacy-db",
|
||||
action="store_true",
|
||||
help="Drop the legacy mini_program database after the shared schema has been applied.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
env_values = load_env_file(Path(args.env_file).expanduser().resolve())
|
||||
args.host = args.host or os.getenv("MYSQL_HOST") or os.getenv("DB_HOST") or env_values.get("DB_HOST") or "127.0.0.1"
|
||||
args.port = (
|
||||
args.port
|
||||
or int(os.getenv("MYSQL_PORT") or os.getenv("DB_PORT") or env_values.get("DB_PORT") or "3306")
|
||||
)
|
||||
args.user = args.user or os.getenv("MYSQL_USER") or os.getenv("DB_USER") or env_values.get("DB_USER") or "root"
|
||||
args.password = (
|
||||
args.password
|
||||
or os.getenv("MYSQL_PASSWORD")
|
||||
or os.getenv("DB_PASSWORD")
|
||||
or env_values.get("DB_PASSWORD")
|
||||
or ""
|
||||
)
|
||||
args.target_db = args.target_db or DEFAULT_TARGET_DB
|
||||
args.legacy_db = args.legacy_db or DEFAULT_LEGACY_DB
|
||||
return args
|
||||
|
||||
|
||||
def split_sql_statements(sql: str) -> list[str]:
|
||||
statements: list[str] = []
|
||||
buffer: list[str] = []
|
||||
quote_char: str | None = None
|
||||
escaped = False
|
||||
|
||||
for char in sql:
|
||||
buffer.append(char)
|
||||
if quote_char is not None:
|
||||
if escaped:
|
||||
escaped = False
|
||||
elif char == "\\" and quote_char in {"'", '"'}:
|
||||
escaped = True
|
||||
elif char == quote_char:
|
||||
quote_char = None
|
||||
continue
|
||||
|
||||
if char in {"'", '"', "`"}:
|
||||
quote_char = char
|
||||
continue
|
||||
|
||||
if char == ";":
|
||||
statement = "".join(buffer).strip()
|
||||
if statement:
|
||||
statements.append(statement)
|
||||
buffer = []
|
||||
|
||||
tail = "".join(buffer).strip()
|
||||
if tail:
|
||||
statements.append(tail)
|
||||
return statements
|
||||
|
||||
|
||||
def load_schema_sql(path: Path, *, target_db: str) -> str:
|
||||
raw_sql = path.read_text(encoding="utf-8-sig")
|
||||
sql_lines = []
|
||||
for line in raw_sql.splitlines():
|
||||
stripped = line.lstrip()
|
||||
if stripped.startswith("--"):
|
||||
continue
|
||||
sql_lines.append(line)
|
||||
sql = "\n".join(sql_lines)
|
||||
sql = sql.replace("CREATE DATABASE IF NOT EXISTS `talkingq`", f"CREATE DATABASE IF NOT EXISTS `{target_db}`")
|
||||
sql = sql.replace("USE `talkingq`;", f"USE `{target_db}`;")
|
||||
return sql
|
||||
|
||||
|
||||
def execute_statements(connection: pymysql.Connection, statements: list[str]) -> None:
|
||||
with connection.cursor() as cursor:
|
||||
for index, statement in enumerate(statements, start=1):
|
||||
try:
|
||||
cursor.execute(statement)
|
||||
except Exception as exc: # pragma: no cover - operational path
|
||||
snippet = " ".join(statement.split())[:200]
|
||||
raise RuntimeError(f"statement #{index} failed: {snippet}") from exc
|
||||
|
||||
|
||||
def fetch_table_names(connection: pymysql.Connection, *, database: str) -> set[str]:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = %s
|
||||
""",
|
||||
(database,),
|
||||
)
|
||||
return {row[0] for row in cursor.fetchall()}
|
||||
|
||||
|
||||
def database_exists(connection: pymysql.Connection, *, database: str) -> bool:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SHOW DATABASES LIKE %s", (database,))
|
||||
return cursor.fetchone() is not None
|
||||
|
||||
|
||||
def drop_database(connection: pymysql.Connection, *, database: str) -> None:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(f"DROP DATABASE IF EXISTS `{database}`")
|
||||
|
||||
|
||||
def print_summary(connection: pymysql.Connection, *, target_db: str) -> None:
|
||||
tables = fetch_table_names(connection, database=target_db)
|
||||
expected_tables = set(TALKINGQ_URL_TABLES + MINI_PROGRAM_TABLES)
|
||||
missing_tables = sorted(expected_tables - tables)
|
||||
|
||||
print(f"target database: {target_db}")
|
||||
print(f"talkingq-url tables present: {len(set(TALKINGQ_URL_TABLES) & tables)}/{len(TALKINGQ_URL_TABLES)}")
|
||||
print(f"mini-program tables present: {len(set(MINI_PROGRAM_TABLES) & tables)}/{len(MINI_PROGRAM_TABLES)}")
|
||||
if missing_tables:
|
||||
print("missing tables:")
|
||||
for table_name in missing_tables:
|
||||
print(f" - {table_name}")
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
schema_path = Path(args.schema).resolve()
|
||||
if not schema_path.exists():
|
||||
print(f"schema file not found: {schema_path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if args.drop_legacy_db and args.legacy_db == args.target_db:
|
||||
print("legacy database must be different from target database", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
schema_sql = load_schema_sql(schema_path, target_db=args.target_db)
|
||||
statements = split_sql_statements(schema_sql)
|
||||
if not statements:
|
||||
print("schema file contains no executable statements", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
connection = pymysql.connect(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
user=args.user,
|
||||
password=args.password,
|
||||
charset="utf8mb4",
|
||||
autocommit=True,
|
||||
)
|
||||
try:
|
||||
print(f"applying shared schema to `{args.target_db}` using `{schema_path}`")
|
||||
print(f"db connection target: {args.user}@{args.host}:{args.port}")
|
||||
if args.env_file:
|
||||
print(f"env fallback file: {Path(args.env_file).expanduser().resolve()}")
|
||||
print("migration mode: talkingq-url data is preserved; legacy mini_program data is ignored")
|
||||
execute_statements(connection, statements)
|
||||
print_summary(connection, target_db=args.target_db)
|
||||
|
||||
if args.drop_legacy_db:
|
||||
if database_exists(connection, database=args.legacy_db):
|
||||
drop_database(connection, database=args.legacy_db)
|
||||
print(f"dropped legacy database: {args.legacy_db}")
|
||||
else:
|
||||
print(f"legacy database not found, skip drop: {args.legacy_db}")
|
||||
else:
|
||||
print(f"legacy database left untouched: {args.legacy_db}")
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
print("shared database migration completed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
479
database/talkingq_shared_schema.sql
Normal file
479
database/talkingq_shared_schema.sql
Normal file
@@ -0,0 +1,479 @@
|
||||
-- Banban shared schema
|
||||
-- Sources:
|
||||
-- 1. talkingq-url/mysql/init/01-init.sql
|
||||
-- 2. mini-program/database/init.sql
|
||||
-- Target database: talkingq
|
||||
--
|
||||
-- Notes:
|
||||
-- 1. talkingq-url base tables are kept as the device-domain foundation.
|
||||
-- 2. mini-program business tables are merged into the same database.
|
||||
-- 3. device_auth.device_id is the shared device identity key.
|
||||
-- 4. Device <-> AI conversation records remain in
|
||||
-- conversation_histories / conversation_messages.
|
||||
-- 5. The database owner is shared, but table ownership is explicit:
|
||||
-- talkingq-url owns device-domain base tables; mini-program owns
|
||||
-- parent/child/binding/im/location extension tables.
|
||||
-- 6. During migration to the shared database, talkingq-url data in
|
||||
-- `talkingq` is the source of truth. Legacy `mini_program` data is
|
||||
-- not merged into `talkingq`.
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS `talkingq`
|
||||
DEFAULT CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE `talkingq`;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- talkingq-url base tables
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `device_configs` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`device_id` VARCHAR(64) NOT NULL,
|
||||
`selected_role_key` VARCHAR(64) NOT NULL,
|
||||
`preferred_language` VARCHAR(10) NULL,
|
||||
`volume` INT NULL,
|
||||
`last_update_time` FLOAT NOT 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 INDEX `device_id_UNIQUE` (`device_id` ASC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `conversation_histories` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`device_id` VARCHAR(64) NOT NULL,
|
||||
`role_key` VARCHAR(64) NOT NULL,
|
||||
`last_interaction_time` FLOAT NOT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_device_id` (`device_id` ASC),
|
||||
INDEX `idx_role_key` (`role_key` ASC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `conversation_messages` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`conversation_id` INT NOT NULL,
|
||||
`is_user` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`content` TEXT NOT NULL,
|
||||
`timestamp` FLOAT NOT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_conversation_id` (`conversation_id` ASC),
|
||||
CONSTRAINT `fk_messages_conversation`
|
||||
FOREIGN KEY (`conversation_id`)
|
||||
REFERENCES `conversation_histories` (`id`)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `roles` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`role_key` VARCHAR(64) NOT NULL,
|
||||
`name` VARCHAR(128) NOT NULL,
|
||||
`description` TEXT NULL,
|
||||
`content` TEXT NOT NULL,
|
||||
`default_language` VARCHAR(10) NULL,
|
||||
`asr_provider` VARCHAR(64) NULL,
|
||||
`llm_provider` VARCHAR(64) NULL,
|
||||
`tts_provider` VARCHAR(64) NULL,
|
||||
`competitive_llm_mode` TINYINT(1) NULL,
|
||||
`volcano_model_id` VARCHAR(64) NULL,
|
||||
`volcano_voice_type` VARCHAR(64) NULL,
|
||||
`tencent_voice_type` VARCHAR(64) NULL,
|
||||
`aliyun_voice_name` VARCHAR(64) NULL,
|
||||
`minimax_voice_id` VARCHAR(64) NULL,
|
||||
`url` VARCHAR(255) NULL,
|
||||
`homophones` JSON 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 INDEX `role_key_UNIQUE` (`role_key` ASC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `role_languages` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`role_id` INT NOT NULL,
|
||||
`language_code` VARCHAR(10) NOT NULL,
|
||||
`name` VARCHAR(128) NULL,
|
||||
`content` TEXT NULL,
|
||||
`asr_provider` VARCHAR(64) NULL,
|
||||
`llm_provider` VARCHAR(64) NULL,
|
||||
`tts_provider` VARCHAR(64) NULL,
|
||||
`volcano_voice_type` VARCHAR(64) NULL,
|
||||
`tencent_voice_type` VARCHAR(64) NULL,
|
||||
`aliyun_voice_name` VARCHAR(64) NULL,
|
||||
`minimax_voice_id` VARCHAR(64) NULL,
|
||||
`url` VARCHAR(255) 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 INDEX `uix_role_language` (`role_id`, `language_code`),
|
||||
CONSTRAINT `fk_role_languages_role`
|
||||
FOREIGN KEY (`role_id`)
|
||||
REFERENCES `roles` (`id`)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `device_auth` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`device_id` VARCHAR(64) NOT NULL,
|
||||
`serial_number` VARCHAR(64) NOT NULL,
|
||||
`batch_id` VARCHAR(20) NULL,
|
||||
`is_active` 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 INDEX `device_id_UNIQUE` (`device_id` ASC),
|
||||
INDEX `idx_batch_id` (`batch_id` ASC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `device_firmware_update` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`device_id` VARCHAR(64) NOT NULL,
|
||||
`serial_number` VARCHAR(64) NOT NULL,
|
||||
`mac_address` VARCHAR(512) NULL,
|
||||
`firmware_version` VARCHAR(64) NOT NULL,
|
||||
`update_status` VARCHAR(32) NOT NULL DEFAULT 'success',
|
||||
`progress` FLOAT NULL DEFAULT 0.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 INDEX `device_id_UNIQUE` (`device_id` ASC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `system_config` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`config_key` VARCHAR(128) NOT NULL,
|
||||
`config_value` TEXT NULL,
|
||||
`description` VARCHAR(255) 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 INDEX `config_key_UNIQUE` (`config_key` ASC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO `device_configs` (`device_id`, `selected_role_key`, `preferred_language`, `last_update_time`)
|
||||
VALUES ('default', 'assistant', 'zh', UNIX_TIMESTAMP())
|
||||
ON DUPLICATE KEY UPDATE `selected_role_key` = `selected_role_key`;
|
||||
|
||||
INSERT INTO `system_config` (`config_key`, `config_value`, `description`)
|
||||
VALUES
|
||||
('latest_firmware_version', '1.0.0', '最新固件版本'),
|
||||
('update_firmware_url', 'https://example.com/firmware/latest.bin', '固件更新URL')
|
||||
ON DUPLICATE KEY UPDATE `config_value` = `config_value`;
|
||||
|
||||
INSERT INTO `roles` (`role_key`, `name`, `description`, `content`, `default_language`, `enabled`)
|
||||
VALUES (
|
||||
'assistant',
|
||||
'智能助手',
|
||||
'默认智能助手角色',
|
||||
'你是一个友好的智能助手,乐于帮助用户解答问题。',
|
||||
'zh',
|
||||
1
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE `role_key` = `role_key`;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- mini-program extension tables
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `parents` (
|
||||
`user_id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`openid` VARCHAR(64) NOT NULL,
|
||||
`unionid` VARCHAR(64) NULL,
|
||||
`nickname` VARCHAR(64) NULL,
|
||||
`avatar_url` VARCHAR(255) NULL,
|
||||
`avatar_file_key` VARCHAR(255) NULL,
|
||||
`phone` VARCHAR(20) NULL,
|
||||
`status` TINYINT 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 (`user_id`),
|
||||
UNIQUE KEY `uq_parents_openid` (`openid`),
|
||||
KEY `idx_parents_unionid` (`unionid`),
|
||||
KEY `idx_parents_phone` (`phone`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `children` (
|
||||
`child_id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`child_name` VARCHAR(32) NOT NULL,
|
||||
`child_gender` TINYINT NOT NULL DEFAULT 2,
|
||||
`child_birthday` DATE NULL,
|
||||
`status` TINYINT 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 (`child_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `parent_child_relations` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`user_id` BIGINT NOT NULL,
|
||||
`child_id` BIGINT NOT NULL,
|
||||
`relation_type` TINYINT NOT NULL DEFAULT 9,
|
||||
`is_primary` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`status` TINYINT 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`),
|
||||
CONSTRAINT `uq_user_child` UNIQUE (`user_id`, `child_id`),
|
||||
KEY `idx_pcr_user_id` (`user_id`),
|
||||
KEY `idx_pcr_child_id` (`child_id`),
|
||||
CONSTRAINT `fk_pcr_user`
|
||||
FOREIGN KEY (`user_id`)
|
||||
REFERENCES `parents` (`user_id`)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_pcr_child`
|
||||
FOREIGN KEY (`child_id`)
|
||||
REFERENCES `children` (`child_id`)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `device_bindings` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`device_id` VARCHAR(64) NOT NULL,
|
||||
`owner_user_id` BIGINT NOT NULL,
|
||||
`child_id` BIGINT NULL,
|
||||
`status` TINYINT NOT NULL DEFAULT 1,
|
||||
`bound_at` DATETIME NOT NULL,
|
||||
`unbound_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`),
|
||||
CONSTRAINT `uq_device_binding_device` UNIQUE (`device_id`),
|
||||
CONSTRAINT `uq_device_binding_child` UNIQUE (`child_id`),
|
||||
KEY `idx_device_bindings_owner_user_id` (`owner_user_id`),
|
||||
CONSTRAINT `fk_device_bindings_device`
|
||||
FOREIGN KEY (`device_id`)
|
||||
REFERENCES `device_auth` (`device_id`),
|
||||
CONSTRAINT `fk_device_bindings_owner`
|
||||
FOREIGN KEY (`owner_user_id`)
|
||||
REFERENCES `parents` (`user_id`),
|
||||
CONSTRAINT `fk_device_bindings_child`
|
||||
FOREIGN KEY (`child_id`)
|
||||
REFERENCES `children` (`child_id`)
|
||||
ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `device_bind_sessions` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`bind_token` CHAR(36) NOT NULL,
|
||||
`device_id` VARCHAR(64) NOT NULL,
|
||||
`initiator_user_id` BIGINT NOT NULL,
|
||||
`target_child_id` BIGINT NULL,
|
||||
`challenge_code_hash` CHAR(64) NULL,
|
||||
`challenge_set_at` DATETIME NULL,
|
||||
`expires_at` DATETIME NOT NULL,
|
||||
`max_attempt_count` TINYINT UNSIGNED NOT NULL DEFAULT 5,
|
||||
`attempt_count` TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`status` TINYINT NOT NULL DEFAULT 1,
|
||||
`confirmed_at` DATETIME 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_device_bind_sessions_token` (`bind_token`),
|
||||
KEY `idx_device_bind_sessions_device_id` (`device_id`),
|
||||
KEY `idx_device_bind_sessions_initiator_user_id` (`initiator_user_id`),
|
||||
KEY `idx_device_bind_sessions_target_child_id` (`target_child_id`),
|
||||
KEY `idx_device_bind_sessions_expires_at` (`expires_at`),
|
||||
CONSTRAINT `fk_device_bind_sessions_device`
|
||||
FOREIGN KEY (`device_id`)
|
||||
REFERENCES `device_auth` (`device_id`),
|
||||
CONSTRAINT `fk_device_bind_sessions_initiator`
|
||||
FOREIGN KEY (`initiator_user_id`)
|
||||
REFERENCES `parents` (`user_id`),
|
||||
CONSTRAINT `fk_device_bind_sessions_target_child`
|
||||
FOREIGN KEY (`target_child_id`)
|
||||
REFERENCES `children` (`child_id`)
|
||||
ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `device_bind_history` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`device_id` VARCHAR(64) NOT NULL,
|
||||
`child_id` BIGINT NULL,
|
||||
`bound_by_user_id` BIGINT NOT NULL,
|
||||
`unbound_by_user_id` BIGINT NULL,
|
||||
`bind_source` TINYINT NOT NULL DEFAULT 1,
|
||||
`bound_at` DATETIME NOT NULL,
|
||||
`unbound_at` DATETIME NULL,
|
||||
`unbind_reason` VARCHAR(191) NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_device_bind_history_device_id` (`device_id`),
|
||||
KEY `idx_device_bind_history_child_id` (`child_id`),
|
||||
KEY `idx_device_bind_history_bound_by_user_id` (`bound_by_user_id`),
|
||||
KEY `idx_device_bind_history_unbound_by_user_id` (`unbound_by_user_id`),
|
||||
KEY `idx_device_bind_history_bound_at` (`bound_at`),
|
||||
CONSTRAINT `fk_device_bind_history_device`
|
||||
FOREIGN KEY (`device_id`)
|
||||
REFERENCES `device_auth` (`device_id`),
|
||||
CONSTRAINT `fk_device_bind_history_child`
|
||||
FOREIGN KEY (`child_id`)
|
||||
REFERENCES `children` (`child_id`)
|
||||
ON DELETE SET NULL,
|
||||
CONSTRAINT `fk_device_bind_history_bound_by`
|
||||
FOREIGN KEY (`bound_by_user_id`)
|
||||
REFERENCES `parents` (`user_id`),
|
||||
CONSTRAINT `fk_device_bind_history_unbound_by`
|
||||
FOREIGN KEY (`unbound_by_user_id`)
|
||||
REFERENCES `parents` (`user_id`)
|
||||
ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `cards` (
|
||||
`card_id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`card_uuid` VARCHAR(64) NOT NULL,
|
||||
`device_id` VARCHAR(64) NULL,
|
||||
`card_name` VARCHAR(64) NULL,
|
||||
`status` TINYINT NOT NULL DEFAULT 0,
|
||||
`total_swaps` INT 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 (`card_id`),
|
||||
UNIQUE KEY `uq_cards_card_uuid` (`card_uuid`),
|
||||
UNIQUE KEY `uq_cards_device_id` (`device_id`),
|
||||
KEY `idx_cards_status` (`status`),
|
||||
CONSTRAINT `fk_cards_device`
|
||||
FOREIGN KEY (`device_id`)
|
||||
REFERENCES `device_auth` (`device_id`)
|
||||
ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Shared device volume is recommended to use device_configs.volume as source of truth.
|
||||
-- device_settings.volume is retained for mini-program compatibility and UI storage.
|
||||
CREATE TABLE IF NOT EXISTS `device_settings` (
|
||||
`setting_id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`device_id` VARCHAR(64) NOT NULL,
|
||||
`sleep_mode` TINYINT NOT NULL DEFAULT 0,
|
||||
`disable_time_start` TIME NULL,
|
||||
`disable_time_end` TIME NULL,
|
||||
`timezone` VARCHAR(32) NOT NULL DEFAULT 'Asia/Shanghai',
|
||||
`volume` TINYINT UNSIGNED NULL,
|
||||
`brightness` TINYINT UNSIGNED NULL,
|
||||
`disable_weekdays` VARCHAR(32) NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`setting_id`),
|
||||
UNIQUE KEY `uq_device_settings_device_id` (`device_id`),
|
||||
CONSTRAINT `fk_device_settings_device`
|
||||
FOREIGN KEY (`device_id`)
|
||||
REFERENCES `device_auth` (`device_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `im_conversations` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`conversation_type` TINYINT UNSIGNED NOT NULL,
|
||||
`participant_a_type` TINYINT UNSIGNED NOT NULL,
|
||||
`participant_a_id` VARCHAR(64) NOT NULL,
|
||||
`participant_b_type` TINYINT UNSIGNED NOT NULL,
|
||||
`participant_b_id` VARCHAR(64) NOT NULL,
|
||||
`pair_key` VARCHAR(191) NOT NULL,
|
||||
`status` TINYINT NOT NULL DEFAULT 1,
|
||||
`last_seq` BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`message_count` BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`last_message_preview` VARCHAR(255) NULL,
|
||||
`last_message_at` DATETIME NULL,
|
||||
`ext_json` JSON NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
CONSTRAINT `uq_conv_type_pair` UNIQUE (`conversation_type`, `pair_key`),
|
||||
KEY `idx_im_conv_participant_a` (`participant_a_type`, `participant_a_id`),
|
||||
KEY `idx_im_conv_participant_b` (`participant_b_type`, `participant_b_id`),
|
||||
KEY `idx_im_conv_last_message_at` (`last_message_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `im_messages` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`conversation_id` BIGINT UNSIGNED NOT NULL,
|
||||
`seq` BIGINT UNSIGNED NOT NULL,
|
||||
`sender_type` TINYINT UNSIGNED NOT NULL,
|
||||
`sender_id` VARCHAR(64) NOT NULL,
|
||||
`receiver_type` TINYINT UNSIGNED NOT NULL,
|
||||
`receiver_id` VARCHAR(64) NOT NULL,
|
||||
`content_type` TINYINT UNSIGNED NOT NULL,
|
||||
`content_text` MEDIUMTEXT NULL,
|
||||
`content_json` JSON NULL,
|
||||
`media_file_key` VARCHAR(255) NULL,
|
||||
`media_duration_ms` INT UNSIGNED NULL,
|
||||
`media_mime_type` VARCHAR(64) NULL,
|
||||
`media_size_bytes` BIGINT UNSIGNED NULL,
|
||||
`media_transcript_text` TEXT NULL,
|
||||
`client_msg_id` VARCHAR(64) NULL,
|
||||
`sender_name_snapshot` VARCHAR(64) NULL,
|
||||
`sender_avatar_snapshot` VARCHAR(255) NULL,
|
||||
`receiver_name_snapshot` VARCHAR(64) NULL,
|
||||
`receiver_avatar_snapshot` VARCHAR(255) NULL,
|
||||
`ext_json` JSON NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`deleted_at` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
CONSTRAINT `uq_im_msg_conv_seq` UNIQUE (`conversation_id`, `seq`),
|
||||
CONSTRAINT `uq_im_msg_client` UNIQUE (`conversation_id`, `client_msg_id`),
|
||||
KEY `idx_im_msg_conversation_created_at` (`conversation_id`, `created_at`),
|
||||
KEY `idx_im_msg_sender` (`sender_type`, `sender_id`, `created_at`),
|
||||
KEY `idx_im_msg_receiver` (`receiver_type`, `receiver_id`, `created_at`),
|
||||
CONSTRAINT `fk_im_messages_conversation`
|
||||
FOREIGN KEY (`conversation_id`)
|
||||
REFERENCES `im_conversations` (`id`)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `child_location_current` (
|
||||
`child_id` BIGINT NOT NULL,
|
||||
`device_id` VARCHAR(64) NOT NULL,
|
||||
`coord_type` VARCHAR(16) NOT NULL DEFAULT 'gcj02',
|
||||
`lat` DECIMAL(10, 7) NOT NULL,
|
||||
`lng` DECIMAL(10, 7) NOT NULL,
|
||||
`accuracy_m` INT UNSIGNED NULL,
|
||||
`altitude_m` DECIMAL(8, 2) NULL,
|
||||
`speed_mps` DECIMAL(8, 2) NULL,
|
||||
`heading_deg` SMALLINT UNSIGNED NULL,
|
||||
`source` TINYINT UNSIGNED NOT NULL,
|
||||
`battery_pct` TINYINT UNSIGNED NULL,
|
||||
`device_time` DATETIME NOT NULL,
|
||||
`server_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`child_id`),
|
||||
KEY `idx_child_loc_current_device` (`device_id`),
|
||||
KEY `idx_child_loc_current_server_time` (`server_time`),
|
||||
CONSTRAINT `fk_child_loc_current_child`
|
||||
FOREIGN KEY (`child_id`)
|
||||
REFERENCES `children` (`child_id`)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_child_loc_current_device`
|
||||
FOREIGN KEY (`device_id`)
|
||||
REFERENCES `device_auth` (`device_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `child_location_history` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`child_id` BIGINT NOT NULL,
|
||||
`device_id` VARCHAR(64) NOT NULL,
|
||||
`coord_type` VARCHAR(16) NOT NULL DEFAULT 'gcj02',
|
||||
`lat` DECIMAL(10, 7) NOT NULL,
|
||||
`lng` DECIMAL(10, 7) NOT NULL,
|
||||
`accuracy_m` INT UNSIGNED NULL,
|
||||
`altitude_m` DECIMAL(8, 2) NULL,
|
||||
`speed_mps` DECIMAL(8, 2) NULL,
|
||||
`heading_deg` SMALLINT UNSIGNED NULL,
|
||||
`source` TINYINT UNSIGNED NOT NULL,
|
||||
`battery_pct` TINYINT UNSIGNED NULL,
|
||||
`device_time` DATETIME NOT NULL,
|
||||
`server_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_child_loc_hist_child` (`child_id`, `created_at`),
|
||||
KEY `idx_child_loc_hist_device` (`device_id`, `created_at`),
|
||||
CONSTRAINT `fk_child_loc_hist_child`
|
||||
FOREIGN KEY (`child_id`)
|
||||
REFERENCES `children` (`child_id`)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_child_loc_hist_device`
|
||||
FOREIGN KEY (`device_id`)
|
||||
REFERENCES `device_auth` (`device_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
Reference in New Issue
Block a user