[1.2] 数据库 DDL - 六张表完整建表 + 初始化脚本
This commit is contained in:
131
scripts/ddl.sql
Normal file
131
scripts/ddl.sql
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Sentinel Home AI - 数据库 DDL
|
||||||
|
-- 版本: v1.0
|
||||||
|
-- 日期: 2026-08-19
|
||||||
|
-- 数据库: MariaDB (MySQL 兼容)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 创建数据库(如不存在)
|
||||||
|
CREATE DATABASE IF NOT EXISTS sentinel_home_ai
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE sentinel_home_ai;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. 任务表 process_tasks
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS process_tasks (
|
||||||
|
task_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
video_path VARCHAR(500) NOT NULL COMMENT '视频在 NAS 上的绝对路径',
|
||||||
|
video_url VARCHAR(500) NOT NULL COMMENT 'Video-Server 提供的下载 URL',
|
||||||
|
status ENUM('PENDING','PROCESSING','SUCCESS','FAILED') DEFAULT 'PENDING',
|
||||||
|
retry_count INT DEFAULT 0,
|
||||||
|
max_retries INT DEFAULT 3,
|
||||||
|
next_retry_at DATETIME NULL COMMENT '下次重试时间(退避策略)',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
error_message TEXT NULL,
|
||||||
|
failure_stage ENUM('download','extract','vlm_visual','vlm_fusion','callback') NULL COMMENT '失败阶段',
|
||||||
|
heartbeat_at DATETIME NULL COMMENT '心跳时间戳(首期预留,不写入)',
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_created (created_at),
|
||||||
|
INDEX idx_next_retry (next_retry_at)
|
||||||
|
) ENGINE=InnoDB COMMENT='视频分析任务表';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. 事件表 monitor_events
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS monitor_events (
|
||||||
|
event_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
task_id INT NOT NULL,
|
||||||
|
event_start_time DATETIME NOT NULL COMMENT '事件开始时间',
|
||||||
|
event_end_time DATETIME NOT NULL COMMENT '事件结束时间',
|
||||||
|
camera_name VARCHAR(50) COMMENT '摄像头名称/位置',
|
||||||
|
global_summary TEXT COMMENT 'VLM 生成的整体摘要',
|
||||||
|
entities_json JSON NOT NULL COMMENT '实体列表 JSON 数组',
|
||||||
|
compute_provider JSON NOT NULL COMMENT '模型来源数组,如 ["ollama","gemini"]',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_event_start (event_start_time),
|
||||||
|
INDEX idx_compute_provider (compute_provider),
|
||||||
|
FOREIGN KEY (task_id) REFERENCES process_tasks(task_id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB COMMENT='监控事件聚合表';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. 每日摘要表 daily_summaries(预留)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS daily_summaries (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
target_date DATE NOT NULL UNIQUE COMMENT '目标日期',
|
||||||
|
summary_text TEXT COMMENT '当日摘要文本',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB COMMENT='每日摘要表(首期预留)';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 事件明细表 event_details
|
||||||
|
-- 每关键帧一条,由 VLM 输出 frame_details 拆分落库
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS event_details (
|
||||||
|
detail_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
event_id INT NOT NULL COMMENT '关联 monitor_events.event_id',
|
||||||
|
task_id INT NOT NULL COMMENT '冗余,便于查询',
|
||||||
|
frame_index INT NOT NULL COMMENT '关键帧序号(1-8)',
|
||||||
|
frame_timestamp DATETIME NOT NULL COMMENT '该帧对应的绝对时间点',
|
||||||
|
camera_name VARCHAR(50) COMMENT '摄像头位置(如"客厅")',
|
||||||
|
person VARCHAR(50) NOT NULL COMMENT '成员名或"人物A/B/C"或"未知访客"',
|
||||||
|
action VARCHAR(200) NOT NULL COMMENT '动作描述(AI 自由生成,无枚举)',
|
||||||
|
clothing VARCHAR(100) COMMENT '衣着描述',
|
||||||
|
is_attention_event BOOLEAN DEFAULT FALSE COMMENT 'AI 自行判断是否为关注事件',
|
||||||
|
source_providers JSON NOT NULL COMMENT '识别到该明细的模型来源数组',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_event_id (event_id),
|
||||||
|
INDEX idx_frame_timestamp (frame_timestamp),
|
||||||
|
INDEX idx_person (person),
|
||||||
|
INDEX idx_queried_date (frame_timestamp),
|
||||||
|
FOREIGN KEY (event_id) REFERENCES monitor_events(event_id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB COMMENT='事件明细表(每关键帧一条)';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. 对话记录表 chat_history
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_history (
|
||||||
|
chat_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_question TEXT NOT NULL COMMENT '用户问题',
|
||||||
|
ai_answer TEXT NOT NULL COMMENT 'AI 回答',
|
||||||
|
context_summary TEXT COMMENT '问答时使用的上下文摘要',
|
||||||
|
queried_date DATE COMMENT '查询的日期范围',
|
||||||
|
queried_person VARCHAR(50) COMMENT '查询的人物',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_created (created_at),
|
||||||
|
INDEX idx_queried_date (queried_date),
|
||||||
|
INDEX idx_queried_person (queried_person)
|
||||||
|
) ENGINE=InnoDB COMMENT='用户与 AI 问答记录表';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 6. 家庭成员表 family_members
|
||||||
|
-- 交互式命名:VLM 自动按特征提取"人物A/B/C",用户命名为真实名字
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS family_members (
|
||||||
|
member_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
abstract_label VARCHAR(20) NOT NULL UNIQUE COMMENT 'VLM 首次提取的抽象标识,如"人物A"',
|
||||||
|
real_name VARCHAR(50) COMMENT '用户命名的真实名字,如"汤圆";NULL 表示未命名',
|
||||||
|
feature_description TEXT COMMENT 'VLM 提取的特征描述',
|
||||||
|
first_seen_at DATETIME COMMENT '首次被提取的时间',
|
||||||
|
named_at DATETIME NULL COMMENT '用户命名的时间',
|
||||||
|
named_by VARCHAR(50) COMMENT '命名人',
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_real_name (real_name),
|
||||||
|
INDEX idx_abstract_label (abstract_label),
|
||||||
|
INDEX idx_is_active (is_active)
|
||||||
|
) ENGINE=InnoDB COMMENT='家庭成员表(交互式命名)';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 验证
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'DDL 执行完成' AS message;
|
||||||
|
SELECT TABLE_NAME, TABLE_COMMENT
|
||||||
|
FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = 'sentinel_home_ai'
|
||||||
|
ORDER BY TABLE_NAME;
|
||||||
125
scripts/init_db.py
Normal file
125
scripts/init_db.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Sentinel Home AI - 数据库初始化脚本
|
||||||
|
执行 DDL 创建所有表,并可插入测试数据。
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
import mysql.connector
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection(host='127.0.0.1', port=3306, user='root', password=''):
|
||||||
|
"""获取 MariaDB 连接"""
|
||||||
|
return mysql.connector.connect(
|
||||||
|
host=host, port=port, user=user, password=password,
|
||||||
|
charset='utf8mb4', collation='utf8mb4_unicode_ci'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def execute_ddl(conn):
|
||||||
|
"""执行 DDL 脚本"""
|
||||||
|
ddl_path = os.path.join(os.path.dirname(__file__), 'ddl.sql')
|
||||||
|
with open(ddl_path, 'r', encoding='utf-8') as f:
|
||||||
|
ddl_sql = f.read()
|
||||||
|
|
||||||
|
cursor = conn.cursor()
|
||||||
|
# 按分号分割执行(简单方案,适合无存储过程的 DDL)
|
||||||
|
statements = [s.strip() for s in ddl_sql.split(';') if s.strip() and not s.strip().startswith('--')]
|
||||||
|
for stmt in statements:
|
||||||
|
if stmt.upper().startswith('SELECT'):
|
||||||
|
cursor.execute(stmt)
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
print(f" {row[0]}: {row[1] if len(row) > 1 else ''}")
|
||||||
|
else:
|
||||||
|
cursor.execute(stmt)
|
||||||
|
print(f" OK: {stmt[:60]}...")
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
print("\n[OK] DDL 执行完成")
|
||||||
|
|
||||||
|
|
||||||
|
def insert_test_data(conn):
|
||||||
|
"""插入测试数据"""
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 插入测试任务
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO process_tasks (video_path, video_url, status)
|
||||||
|
VALUES (%s, %s, %s)
|
||||||
|
""", ('/volume1/surveillance/test.mp4', 'http://100.x.x.10:8000/media/test.mp4?token=xxx', 'SUCCESS'))
|
||||||
|
|
||||||
|
task_id = cursor.lastrowid
|
||||||
|
|
||||||
|
# 插入测试事件
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO monitor_events (task_id, event_start_time, event_end_time, camera_name, global_summary, entities_json, compute_provider)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||||
|
""", (
|
||||||
|
task_id,
|
||||||
|
'2026-08-19 14:00:00',
|
||||||
|
'2026-08-19 14:30:00',
|
||||||
|
'客厅',
|
||||||
|
'客厅有人员走动,汤圆在玩积木。',
|
||||||
|
'[{"person": "人物A", "action": "在地毯上玩积木", "clothing": "黄色T恤"}]',
|
||||||
|
'["ollama"]'
|
||||||
|
))
|
||||||
|
event_id = cursor.lastrowid
|
||||||
|
|
||||||
|
# 插入测试事件明细
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO event_details (event_id, task_id, frame_index, frame_timestamp, camera_name, person, action, clothing, is_attention_event, source_providers)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
""", (
|
||||||
|
event_id, task_id, 1, '2026-08-19 14:03:00', '客厅', '人物A', '在地毯上玩积木', '黄色T恤', False, '["ollama"]'
|
||||||
|
))
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO event_details (event_id, task_id, frame_index, frame_timestamp, camera_name, person, action, clothing, is_attention_event, source_providers)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
""", (
|
||||||
|
event_id, task_id, 2, '2026-08-19 14:15:00', '客厅', '人物A', '看绘本', '黄色T恤', False, '["ollama"]'
|
||||||
|
))
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO event_details (event_id, task_id, frame_index, frame_timestamp, camera_name, person, action, clothing, is_attention_event, source_providers)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
""", (
|
||||||
|
event_id, task_id, 3, '2026-08-19 14:25:00', '客厅', '人物A', '跌倒', '黄色T恤', True, '["ollama"]'
|
||||||
|
))
|
||||||
|
|
||||||
|
# 插入测试成员
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT IGNORE INTO family_members (abstract_label, real_name, feature_description, first_seen_at)
|
||||||
|
VALUES (%s, %s, %s, %s)
|
||||||
|
""", ('人物A', None, '短发、黄色T恤、男性', '2026-08-19 14:03:00'))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
print(f"[OK] 测试数据插入完成 (task_id={task_id}, event_id={event_id})")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='Sentinel Home AI 数据库初始化')
|
||||||
|
parser.add_argument('--host', default='127.0.0.1', help='MariaDB 主机')
|
||||||
|
parser.add_argument('--port', type=int, default=3306, help='MariaDB 端口')
|
||||||
|
parser.add_argument('--user', default='root', help='MariaDB 用户名')
|
||||||
|
parser.add_argument('--password', default='', help='MariaDB 密码')
|
||||||
|
parser.add_argument('--test-data', action='store_true', help='插入测试数据')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print(f"连接 MariaDB {args.host}:{args.port} ...")
|
||||||
|
conn = get_connection(args.host, args.port, args.user, args.password)
|
||||||
|
|
||||||
|
print("执行 DDL ...")
|
||||||
|
execute_ddl(conn)
|
||||||
|
|
||||||
|
if args.test_data:
|
||||||
|
print("\n插入测试数据 ...")
|
||||||
|
insert_test_data(conn)
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
print("\n完成!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user