Commit 2acacd1a01444e011e0ffaa24ab8518fcd55b6e2
1 parent
3785d15b
feat: add admin binding portal (wechat style)
Showing
3 changed files
with
417 additions
and
106 deletions
admin/index.php
0 → 100644
| 1 | +<?php | |
| 2 | +require_once __DIR__ . '/../vendor/autoload.php'; | |
| 3 | + | |
| 4 | +// 加载配置 (.env 手动加载逻辑同 start.php) | |
| 5 | +if (file_exists(__DIR__ . '/../.env')) { | |
| 6 | + $lines = file(__DIR__ . '/../.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); | |
| 7 | + foreach ($lines as $line) { | |
| 8 | + if (strpos(trim($line), '#') === 0) | |
| 9 | + continue; | |
| 10 | + list($name, $value) = explode('=', $line, 2); | |
| 11 | + $_ENV[trim($name)] = trim($value); | |
| 12 | + } | |
| 13 | +} | |
| 14 | + | |
| 15 | +// 简单的一致性检查 (实际生产环境应加上 Session 登录验证) | |
| 16 | +// 这里假设通过 Basic Auth 或内网访问 | |
| 17 | + | |
| 18 | +// 连接数据库 | |
| 19 | +try { | |
| 20 | + $dsn = "mysql:host={$_ENV['DB_HOST']};dbname={$_ENV['DB_NAME']};charset=utf8mb4"; | |
| 21 | + $pdo = new PDO($dsn, $_ENV['DB_USER'], $_ENV['DB_PASS']); | |
| 22 | + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); | |
| 23 | +} catch (PDOException $e) { | |
| 24 | + die("Database connection failed: " . $e->getMessage()); | |
| 25 | +} | |
| 26 | + | |
| 27 | +// 处理绑定表单提交 | |
| 28 | +$message = ''; | |
| 29 | +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'bind') { | |
| 30 | + $deviceId = trim($_POST['device_id']); | |
| 31 | + $phone = trim($_POST['phone']); | |
| 32 | + $isPrimary = isset($_POST['is_primary']) ? 1 : 0; | |
| 33 | + | |
| 34 | + if ($deviceId && $phone) { | |
| 35 | + // 1. 查找用户 | |
| 36 | + $stmt = $pdo->prepare("SELECT id, nickname FROM users WHERE phone = ?"); | |
| 37 | + $stmt->execute([$phone]); | |
| 38 | + $user = $stmt->fetch(PDO::FETCH_ASSOC); | |
| 39 | + | |
| 40 | + if ($user) { | |
| 41 | + // 2. 插入/更新绑定 | |
| 42 | + // 先检查是否已存在 | |
| 43 | + $check = $pdo->prepare("SELECT id FROM user_device_bindings WHERE user_id = ? AND device_id = ?"); | |
| 44 | + $check->execute([$user['id'], $deviceId]); | |
| 45 | + | |
| 46 | + if (!$check->fetch()) { | |
| 47 | + $bind = $pdo->prepare("INSERT INTO user_device_bindings (user_id, device_id, is_primary, created_at) VALUES (?, ?, ?, NOW())"); | |
| 48 | + $bind->execute([$user['id'], $deviceId, $isPrimary]); | |
| 49 | + $message = "<div style='color: green; margin-bottom: 20px;'>✅ 成功将设备 <b>$deviceId</b> 绑定给用户 <b>{$user['nickname']}</b></div>"; | |
| 50 | + } else { | |
| 51 | + $message = "<div style='color: orange; margin-bottom: 20px;'>⚠️ 该用户已经绑定过此设备,无需重复操作。</div>"; | |
| 52 | + } | |
| 53 | + } else { | |
| 54 | + $message = "<div style='color: red; margin-bottom: 20px;'>❌ 手机号 <b>$phone</b> 未找到。请确保用户已在小程序登录过。</div>"; | |
| 55 | + } | |
| 56 | + } | |
| 57 | +} | |
| 58 | + | |
| 59 | +// 获取设备列表 (Mock Data + Binding Count) | |
| 60 | +// 实际项目应从 Redis 或 devices 表获取在线状态,这里先从 bindings 表反查活跃情况 | |
| 61 | +// 为了简化,我们列出 distinct device_id from bindings,或者列出 bindings | |
| 62 | +$bindings = $pdo->query(" | |
| 63 | + SELECT b.id, u.nickname, u.phone, b.device_id, b.is_primary, b.created_at | |
| 64 | + FROM user_device_bindings b | |
| 65 | + JOIN users u ON b.user_id = u.id | |
| 66 | + ORDER BY b.created_at DESC | |
| 67 | +")->fetchAll(PDO::FETCH_ASSOC); | |
| 68 | + | |
| 69 | +?> | |
| 70 | +<!DOCTYPE html> | |
| 71 | +<html lang="zh-CN"> | |
| 72 | + | |
| 73 | +<head> | |
| 74 | + <meta charset="UTF-8"> | |
| 75 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 76 | + <title>Moltbot 管理后台</title> | |
| 77 | + <link rel="stylesheet" href="style.css"> | |
| 78 | +</head> | |
| 79 | + | |
| 80 | +<body> | |
| 81 | + | |
| 82 | + <div class="header"> | |
| 83 | + <div class="brand">Moltbot Admin</div> | |
| 84 | + <div> | |
| 85 | + <?php echo date('Y-m-d H:i'); ?> | |
| 86 | + </div> | |
| 87 | + </div> | |
| 88 | + | |
| 89 | + <div class="container"> | |
| 90 | + | |
| 91 | + <?php echo $message; ?> | |
| 92 | + | |
| 93 | + <!-- 新增绑定卡片 --> | |
| 94 | + <div class="card"> | |
| 95 | + <div class="title">新增绑定</div> | |
| 96 | + <form method="POST" action=""> | |
| 97 | + <input type="hidden" name="action" value="bind"> | |
| 98 | + <div style="display: flex; gap: 20px;"> | |
| 99 | + <div class="form-group" style="flex: 1;"> | |
| 100 | + <label class="form-label">设备 ID</label> | |
| 101 | + <input type="text" name="device_id" class="form-control" placeholder="例如: dev_test_001" | |
| 102 | + required> | |
| 103 | + </div> | |
| 104 | + <div class="form-group" style="flex: 1;"> | |
| 105 | + <label class="form-label">用户手机号</label> | |
| 106 | + <input type="text" name="phone" class="form-control" placeholder="输入用户注册手机号" required> | |
| 107 | + </div> | |
| 108 | + </div> | |
| 109 | + <div class="form-group"> | |
| 110 | + <label> | |
| 111 | + <input type="checkbox" name="is_primary" value="1" checked> 设为主设备 (默认) | |
| 112 | + </label> | |
| 113 | + </div> | |
| 114 | + <button type="submit" class="btn btn-primary">立即绑定</button> | |
| 115 | + </form> | |
| 116 | + </div> | |
| 117 | + | |
| 118 | + <!-- 绑定列表卡片 --> | |
| 119 | + <div class="card"> | |
| 120 | + <div class="title">绑定记录 ( | |
| 121 | + <?php echo count($bindings); ?>) | |
| 122 | + </div> | |
| 123 | + <table> | |
| 124 | + <thead> | |
| 125 | + <tr> | |
| 126 | + <th>用户</th> | |
| 127 | + <th>手机号</th> | |
| 128 | + <th>设备 ID</th> | |
| 129 | + <th>主设备</th> | |
| 130 | + <th>绑定时间</th> | |
| 131 | + <th>操作</th> | |
| 132 | + </tr> | |
| 133 | + </thead> | |
| 134 | + <tbody> | |
| 135 | + <?php foreach ($bindings as $row): ?> | |
| 136 | + <tr> | |
| 137 | + <td> | |
| 138 | + <?php echo htmlspecialchars($row['nickname']); ?> | |
| 139 | + </td> | |
| 140 | + <td> | |
| 141 | + <?php echo htmlspecialchars($row['phone']); ?> | |
| 142 | + </td> | |
| 143 | + <td> | |
| 144 | + <?php echo htmlspecialchars($row['device_id']); ?> | |
| 145 | + </td> | |
| 146 | + <td> | |
| 147 | + <?php if ($row['is_primary']): ?> | |
| 148 | + <span style="color: var(--primary-color);">✔</span> | |
| 149 | + <?php endif; ?> | |
| 150 | + </td> | |
| 151 | + <td> | |
| 152 | + <?php echo $row['created_at']; ?> | |
| 153 | + </td> | |
| 154 | + <td> | |
| 155 | + <a href="#" style="color: red; font-size: 12px; text-decoration: none;">解绑</a> | |
| 156 | + </td> | |
| 157 | + </tr> | |
| 158 | + <?php endforeach; ?> | |
| 159 | + <?php if (empty($bindings)): ?> | |
| 160 | + <tr> | |
| 161 | + <td colspan="6" style="text-align: center; color: #999;">暂无数据</td> | |
| 162 | + </tr> | |
| 163 | + <?php endif; ?> | |
| 164 | + </tbody> | |
| 165 | + </table> | |
| 166 | + </div> | |
| 167 | + | |
| 168 | + </div> | |
| 169 | + | |
| 170 | +</body> | |
| 171 | + | |
| 172 | +</html> | |
| \ No newline at end of file | ... | ... |
admin/style.css
0 → 100644
| 1 | +/* Admin Style - WeUI Inspired */ | |
| 2 | +:root { | |
| 3 | + --primary-color: #07c160; | |
| 4 | + --primary-hover: #06ad56; | |
| 5 | + --bg-color: #f7f7f7; | |
| 6 | + --card-bg: #ffffff; | |
| 7 | + --text-main: #000000; | |
| 8 | + --text-secondary: #888888; | |
| 9 | + --border-color: #e5e5e5; | |
| 10 | +} | |
| 11 | + | |
| 12 | +body { | |
| 13 | + margin: 0; | |
| 14 | + padding: 0; | |
| 15 | + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| 16 | + background-color: var(--bg-color); | |
| 17 | + color: var(--text-main); | |
| 18 | +} | |
| 19 | + | |
| 20 | +.header { | |
| 21 | + background-color: var(--card-bg); | |
| 22 | + padding: 15px 30px; | |
| 23 | + border-bottom: 1px solid var(--border-color); | |
| 24 | + display: flex; | |
| 25 | + justify-content: space-between; | |
| 26 | + align-items: center; | |
| 27 | +} | |
| 28 | + | |
| 29 | +.brand { | |
| 30 | + font-size: 18px; | |
| 31 | + font-weight: 600; | |
| 32 | + color: var(--text-main); | |
| 33 | +} | |
| 34 | + | |
| 35 | +.container { | |
| 36 | + max-width: 1000px; | |
| 37 | + margin: 30px auto; | |
| 38 | + padding: 0 20px; | |
| 39 | +} | |
| 40 | + | |
| 41 | +.card { | |
| 42 | + background-color: var(--card-bg); | |
| 43 | + border-radius: 8px; | |
| 44 | + box-shadow: 0 1px 3px rgba(0,0,0,0.05); | |
| 45 | + padding: 24px; | |
| 46 | + margin-bottom: 24px; | |
| 47 | +} | |
| 48 | + | |
| 49 | +.title { | |
| 50 | + font-size: 20px; | |
| 51 | + font-weight: 500; | |
| 52 | + margin-bottom: 24px; | |
| 53 | + border-left: 4px solid var(--primary-color); | |
| 54 | + padding-left: 12px; | |
| 55 | +} | |
| 56 | + | |
| 57 | +/* Table */ | |
| 58 | +table { | |
| 59 | + width: 100%; | |
| 60 | + border-collapse: collapse; | |
| 61 | +} | |
| 62 | + | |
| 63 | +th { | |
| 64 | + text-align: left; | |
| 65 | + color: var(--text-secondary); | |
| 66 | + font-weight: normal; | |
| 67 | + font-size: 14px; | |
| 68 | + padding: 10px; | |
| 69 | + border-bottom: 1px solid var(--border-color); | |
| 70 | +} | |
| 71 | + | |
| 72 | +td { | |
| 73 | + padding: 12px 10px; | |
| 74 | + border-bottom: 1px solid var(--border-color); | |
| 75 | + font-size: 14px; | |
| 76 | +} | |
| 77 | + | |
| 78 | +.status-dot { | |
| 79 | + display: inline-block; | |
| 80 | + width: 8px; | |
| 81 | + height: 8px; | |
| 82 | + border-radius: 50%; | |
| 83 | + margin-right: 6px; | |
| 84 | +} | |
| 85 | + | |
| 86 | +.status-online { background-color: var(--primary-color); } | |
| 87 | +.status-offline { background-color: #ccc; } | |
| 88 | + | |
| 89 | +/* Buttons */ | |
| 90 | +.btn { | |
| 91 | + display: inline-block; | |
| 92 | + padding: 8px 16px; | |
| 93 | + border-radius: 4px; | |
| 94 | + text-decoration: none; | |
| 95 | + font-size: 14px; | |
| 96 | + cursor: pointer; | |
| 97 | + border: none; | |
| 98 | + transition: background 0.2s; | |
| 99 | +} | |
| 100 | + | |
| 101 | +.btn-primary { | |
| 102 | + background-color: var(--primary-color); | |
| 103 | + color: white; | |
| 104 | +} | |
| 105 | + | |
| 106 | +.btn-primary:hover { | |
| 107 | + background-color: var(--primary-hover); | |
| 108 | +} | |
| 109 | + | |
| 110 | +.btn-sm { | |
| 111 | + padding: 4px 10px; | |
| 112 | + font-size: 12px; | |
| 113 | +} | |
| 114 | + | |
| 115 | +/* Forms */ | |
| 116 | +.form-group { | |
| 117 | + margin-bottom: 16px; | |
| 118 | +} | |
| 119 | + | |
| 120 | +.form-label { | |
| 121 | + display: block; | |
| 122 | + margin-bottom: 8px; | |
| 123 | + color: var(--text-secondary); | |
| 124 | + font-size: 14px; | |
| 125 | +} | |
| 126 | + | |
| 127 | +.form-control { | |
| 128 | + width: 100%; | |
| 129 | + padding: 10px; | |
| 130 | + border: 1px solid var(--border-color); | |
| 131 | + border-radius: 4px; | |
| 132 | + font-size: 14px; | |
| 133 | + box-sizing: border-box; | |
| 134 | +} | |
| 135 | + | |
| 136 | +.form-control:focus { | |
| 137 | + outline: none; | |
| 138 | + border-color: var(--primary-color); | |
| 139 | +} | ... | ... |
| 1 | -<?php | |
| 2 | -require_once __DIR__ . '/vendor/autoload.php'; | |
| 3 | - | |
| 4 | -// Load .env (simple loader since we don't have dotenv lib installed in the basic container, | |
| 5 | -// or relying on docker-compose env injection) | |
| 6 | -// Actually docker-compose injects env vars, so getenv() works. | |
| 7 | - | |
| 8 | -$host = getenv('DB_HOST'); | |
| 9 | -$user = getenv('DB_USER'); | |
| 10 | -$pass = getenv('DB_PASSWORD'); | |
| 11 | -$name = getenv('DB_NAME'); | |
| 12 | -$port = getenv('DB_PORT') ?: 3306; | |
| 13 | - | |
| 14 | -echo "Connecting to MySQL at $host...\n"; | |
| 15 | - | |
| 16 | -try { | |
| 17 | - $dsn = "mysql:host=$host;port=$port;charset=utf8mb4"; | |
| 18 | - $pdo = new PDO($dsn, $user, $pass, [ | |
| 19 | - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, | |
| 20 | - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC | |
| 21 | - ]); | |
| 22 | - | |
| 23 | - echo "Connected successfully.\n"; | |
| 24 | - | |
| 25 | - // Create Database if not exists | |
| 26 | - $pdo->exec("CREATE DATABASE IF NOT EXISTS `$name` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); | |
| 27 | - $pdo->exec("USE `$name`"); | |
| 28 | - echo "Selected database '$name'.\n"; | |
| 29 | - | |
| 30 | - $sql = <<<SQL | |
| 31 | --- Devices Table | |
| 32 | -CREATE TABLE IF NOT EXISTS `devices` ( | |
| 33 | - `id` VARCHAR(64) NOT NULL COMMENT '设备ID (Device ID)', | |
| 34 | - `secret` VARCHAR(128) NOT NULL COMMENT '连接密钥 (Hash)', | |
| 35 | - `name` VARCHAR(50) DEFAULT NULL COMMENT '设备昵称', | |
| 36 | - `status` ENUM('online', 'offline') DEFAULT 'offline' COMMENT '在线状态', | |
| 37 | - `last_seen` TIMESTAMP NULL DEFAULT NULL COMMENT '最后心跳时间', | |
| 38 | - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| 39 | - `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | |
| 40 | - PRIMARY KEY (`id`) | |
| 41 | -) ENGINE=InnoDB COMMENT='设备管理表'; | |
| 42 | - | |
| 43 | --- Users Table | |
| 44 | -CREATE TABLE IF NOT EXISTS `users` ( | |
| 45 | - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, | |
| 46 | - `phone` VARCHAR(20) NOT NULL COMMENT '手机号', | |
| 47 | - `wx_openid` VARCHAR(64) DEFAULT NULL COMMENT '微信 OpenID', | |
| 48 | - `wx_unionid` VARCHAR(64) DEFAULT NULL COMMENT '微信 UnionID', | |
| 49 | - `nickname` VARCHAR(50) DEFAULT NULL COMMENT '显示的昵称', | |
| 50 | - `avatar_url` VARCHAR(255) DEFAULT NULL, | |
| 51 | - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| 52 | - `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | |
| 53 | - PRIMARY KEY (`id`), | |
| 54 | - UNIQUE KEY `uk_phone` (`phone`), | |
| 55 | - KEY `idx_wx_openid` (`wx_openid`) | |
| 56 | -) ENGINE=InnoDB COMMENT='用户表'; | |
| 57 | - | |
| 58 | --- Sessions Table (用户登录会话) | |
| 59 | -CREATE TABLE IF NOT EXISTS `sessions` ( | |
| 60 | - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, | |
| 61 | - `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', | |
| 62 | - `token` VARCHAR(128) NOT NULL COMMENT 'Session Token', | |
| 63 | - `device_info` JSON DEFAULT NULL COMMENT '登录设备信息', | |
| 64 | - `expires_at` TIMESTAMP NOT NULL COMMENT '过期时间', | |
| 65 | - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| 66 | - PRIMARY KEY (`id`), | |
| 67 | - UNIQUE KEY `uk_token` (`token`), | |
| 68 | - KEY `idx_user` (`user_id`), | |
| 69 | - KEY `idx_expires` (`expires_at`) | |
| 70 | -) ENGINE=InnoDB COMMENT='用户会话表'; | |
| 71 | - | |
| 72 | --- User Device Bindings Table (手机号-设备绑定关系) | |
| 73 | -CREATE TABLE IF NOT EXISTS `user_device_bindings` ( | |
| 74 | - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, | |
| 75 | - `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', | |
| 76 | - `device_id` VARCHAR(64) NOT NULL COMMENT '设备ID', | |
| 77 | - `is_primary` TINYINT(1) DEFAULT 0 COMMENT '是否主设备', | |
| 78 | - `bound_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '绑定时间', | |
| 79 | - PRIMARY KEY (`id`), | |
| 80 | - UNIQUE KEY `uk_user_device` (`user_id`, `device_id`), | |
| 81 | - KEY `idx_device` (`device_id`) | |
| 82 | -) ENGINE=InnoDB COMMENT='用户设备绑定表'; | |
| 83 | - | |
| 84 | --- Device Logs Table | |
| 85 | -CREATE TABLE IF NOT EXISTS `device_logs` ( | |
| 86 | - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, | |
| 87 | - `device_id` VARCHAR(64) NOT NULL, | |
| 88 | - `level` VARCHAR(10) DEFAULT 'INFO', | |
| 89 | - `message` TEXT NOT NULL, | |
| 90 | - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| 91 | - PRIMARY KEY (`id`), | |
| 92 | - KEY `idx_device_time` (`device_id`, `created_at`) | |
| 93 | -) ENGINE=InnoDB COMMENT='设备日志表'; | |
| 94 | -SQL; | |
| 95 | - | |
| 96 | - $pdo->exec($sql); | |
| 97 | - echo "Tables created successfully.\n"; | |
| 98 | - | |
| 99 | - // Insert sample device if not exists | |
| 100 | - $pdo->exec("INSERT IGNORE INTO `devices` (`id`, `secret`, `name`, `status`) VALUES ('dev_test_001', '123456', 'My First Moltbot', 'offline')"); | |
| 101 | - echo "Sample data inserted.\n"; | |
| 102 | - | |
| 103 | -} catch (PDOException $e) { | |
| 104 | - echo "Database Error: " . $e->getMessage() . "\n"; | |
| 105 | - exit(1); | |
| 106 | -} | |
| 1 | +<?php | |
| 2 | +require_once __DIR__ . '/vendor/autoload.php'; | |
| 3 | + | |
| 4 | +// Load .env (simple loader since we don't have dotenv lib installed in the basic container, | |
| 5 | +// or relying on docker-compose env injection) | |
| 6 | +// Actually docker-compose injects env vars, so getenv() works. | |
| 7 | + | |
| 8 | +$host = getenv('DB_HOST'); | |
| 9 | +$user = getenv('DB_USER'); | |
| 10 | +$pass = getenv('DB_PASSWORD'); | |
| 11 | +$name = getenv('DB_NAME'); | |
| 12 | +$port = getenv('DB_PORT') ?: 3306; | |
| 13 | + | |
| 14 | +echo "Connecting to MySQL at $host...\n"; | |
| 15 | + | |
| 16 | +try { | |
| 17 | + $dsn = "mysql:host=$host;port=$port;charset=utf8mb4"; | |
| 18 | + $pdo = new PDO($dsn, $user, $pass, [ | |
| 19 | + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, | |
| 20 | + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC | |
| 21 | + ]); | |
| 22 | + | |
| 23 | + echo "Connected successfully.\n"; | |
| 24 | + | |
| 25 | + // Create Database if not exists | |
| 26 | + $pdo->exec("CREATE DATABASE IF NOT EXISTS `$name` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); | |
| 27 | + $pdo->exec("USE `$name`"); | |
| 28 | + echo "Selected database '$name'.\n"; | |
| 29 | + | |
| 30 | + $sql = <<<SQL | |
| 31 | +-- Devices Table | |
| 32 | +CREATE TABLE IF NOT EXISTS `devices` ( | |
| 33 | + `id` VARCHAR(64) NOT NULL COMMENT '设备ID (Device ID)', | |
| 34 | + `secret` VARCHAR(128) NOT NULL COMMENT '连接密钥 (Hash)', | |
| 35 | + `name` VARCHAR(50) DEFAULT NULL COMMENT '设备昵称', | |
| 36 | + `status` ENUM('online', 'offline') DEFAULT 'offline' COMMENT '在线状态', | |
| 37 | + `last_seen` TIMESTAMP NULL DEFAULT NULL COMMENT '最后心跳时间', | |
| 38 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| 39 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | |
| 40 | + PRIMARY KEY (`id`) | |
| 41 | +) ENGINE=InnoDB COMMENT='设备管理表'; | |
| 42 | + | |
| 43 | +-- Users Table | |
| 44 | +CREATE TABLE IF NOT EXISTS `users` ( | |
| 45 | + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, | |
| 46 | + `phone` VARCHAR(20) NOT NULL COMMENT '手机号', | |
| 47 | + `wx_openid` VARCHAR(64) DEFAULT NULL COMMENT '微信 OpenID', | |
| 48 | + `wx_unionid` VARCHAR(64) DEFAULT NULL COMMENT '微信 UnionID', | |
| 49 | + `nickname` VARCHAR(50) DEFAULT NULL COMMENT '显示的昵称', | |
| 50 | + `avatar_url` VARCHAR(255) DEFAULT NULL, | |
| 51 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| 52 | + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | |
| 53 | + PRIMARY KEY (`id`), | |
| 54 | + UNIQUE KEY `uk_phone` (`phone`), | |
| 55 | + KEY `idx_wx_openid` (`wx_openid`) | |
| 56 | +) ENGINE=InnoDB COMMENT='用户表'; | |
| 57 | + | |
| 58 | +-- Sessions Table (用户登录会话) | |
| 59 | +CREATE TABLE IF NOT EXISTS `sessions` ( | |
| 60 | + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, | |
| 61 | + `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', | |
| 62 | + `token` VARCHAR(128) NOT NULL COMMENT 'Session Token', | |
| 63 | + `device_info` JSON DEFAULT NULL COMMENT '登录设备信息', | |
| 64 | + `expires_at` TIMESTAMP NOT NULL COMMENT '过期时间', | |
| 65 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| 66 | + PRIMARY KEY (`id`), | |
| 67 | + UNIQUE KEY `uk_token` (`token`), | |
| 68 | + KEY `idx_user` (`user_id`), | |
| 69 | + KEY `idx_expires` (`expires_at`) | |
| 70 | +) ENGINE=InnoDB COMMENT='用户会话表'; | |
| 71 | + | |
| 72 | +-- User Device Bindings Table (手机号-设备绑定关系) | |
| 73 | +CREATE TABLE IF NOT EXISTS `user_device_bindings` ( | |
| 74 | + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, | |
| 75 | + `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', | |
| 76 | + `device_id` VARCHAR(64) NOT NULL COMMENT '设备ID', | |
| 77 | + `is_primary` TINYINT(1) DEFAULT 0 COMMENT '是否主设备', | |
| 78 | + `bound_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '绑定时间', | |
| 79 | + PRIMARY KEY (`id`), | |
| 80 | + UNIQUE KEY `uk_user_device` (`user_id`, `device_id`), | |
| 81 | + KEY `idx_device` (`device_id`) | |
| 82 | +) ENGINE=InnoDB COMMENT='用户设备绑定表'; | |
| 83 | + | |
| 84 | +-- Device Logs Table | |
| 85 | +CREATE TABLE IF NOT EXISTS `device_logs` ( | |
| 86 | + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, | |
| 87 | + `device_id` VARCHAR(64) NOT NULL, | |
| 88 | + `level` VARCHAR(10) DEFAULT 'INFO', | |
| 89 | + `message` TEXT NOT NULL, | |
| 90 | + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| 91 | + PRIMARY KEY (`id`), | |
| 92 | + KEY `idx_device_time` (`device_id`, `created_at`) | |
| 93 | +) ENGINE=InnoDB COMMENT='设备日志表'; | |
| 94 | +SQL; | |
| 95 | + | |
| 96 | + $pdo->exec($sql); | |
| 97 | + echo "Tables created successfully.\n"; | |
| 98 | + | |
| 99 | + // Insert sample device if not exists | |
| 100 | + $pdo->exec("INSERT IGNORE INTO `devices` (`id`, `secret`, `name`, `status`) VALUES ('dev_test_001', '123456', 'My First Moltbot', 'offline')"); | |
| 101 | + echo "Sample data inserted.\n"; | |
| 102 | + | |
| 103 | +} catch (PDOException $e) { | |
| 104 | + echo "Database Error: " . $e->getMessage() . "\n"; | |
| 105 | + exit(1); | |
| 106 | +} | ... | ... |
Please
register
or
login
to post a comment