<?php session_start(); $lockFile = __DIR__ . '/../data/install.lock'; $installed = file_exists($lockFile) && file_exists(__DIR__ . '/../includes/config.php'); if (!$installed) { header('Location: ../install/index.php'); exit; } require_once __DIR__ . '/../includes/config.php'; require_once __DIR__ . '/../includes/AntiDuplicate.php'; require_once __DIR__ . '/../includes/database.php'; $db = Database::getInstance(); $error = ''; $success = ''; $isLoggedIn = false; if (isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in']) { $isLoggedIn = true; } if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { $action = $_POST['action']; if (in_array($action, ['login', 'generate_keys', 'cleanup', 'expire_link', 'update_content'])) { $formId = $action; if (!AntiDuplicate::validateToken($formId)) { if (AntiDuplicate::checkDuplicateRequest($_POST, 5)) { $error = '⚠️ 检测到重复请求,请稍后再试!'; logMessage("Duplicate request detected: $action from {$_SERVER['REMOTE_ADDR']}", 'WARNING'); } else { $error = '⚠️ 请求无效或已过期,请刷新页面重试!'; } } } if (empty($error)) { if ($action === 'login' && !$isLoggedIn) { $username = $_POST['username'] ?? ''; $password = $_POST['password'] ?? ''; if (defined('ADMIN_USER') && defined('ADMIN_PASS_HASH')) { if ($username === ADMIN_USER && password_verify($password, ADMIN_PASS_HASH)) { $_SESSION['admin_logged_in'] = true; $isLoggedIn = true; logMessage("Admin logged in successfully", 'INFO'); } else { $error = '用户名或密码错误'; logMessage("Failed admin login attempt from: " . ($_SERVER['REMOTE_ADDR'] ?? 'unknown'), 'WARNING'); } } else { $error = '系统配置错误'; } } elseif ($action === 'generate_keys' && $isLoggedIn) { $count = intval($_POST['count'] ?? 1); if ($count > 0 && $count <= 100) { try { $keys = $db->generateKamiKey($count); $success = "成功生成 {$count} 个卡密!"; } catch (Exception $e) { $error = '生成卡密失败: ' . $e->getMessage(); } } else { $error = '请输入1-100之间的数量'; } } elseif ($action === 'cleanup' && $isLoggedIn) { $cleanedCount = $db->cleanupExpiredLinks(); $success = "清理完成,共删除 {$cleanedCount} 个过期临时链接!"; } elseif ($action === 'expire_link' && $isLoggedIn) { $linkId = trim($_POST['link_id'] ?? ''); if ($linkId) { if ($db->manuallyExpireLink($linkId)) { $success = "链接 {$linkId} 已成功设为过期!"; } else { $error = "设为过期失败,找不到链接 {$linkId}!"; } } } elseif ($action === 'update_content' && $isLoggedIn) { $contentKey = trim($_POST['content_key'] ?? ''); $contentValue = trim($_POST['content_value'] ?? ''); if (!empty($contentKey) && !empty($contentValue)) { if ($db->updateContent($contentKey, $contentValue)) { $success = "内容更新成功!"; } else { $error = "内容更新失败!"; } } else { $error = "内容不能为空!"; } } elseif ($action === 'logout' && $isLoggedIn) { session_destroy(); header('Location: index.php'); exit; } } if ($_SERVER['REQUEST_METHOD'] === 'POST' && (!empty($success) || empty($error))) { $_SESSION['success_msg'] = $success; $_SESSION['error_msg'] = $error; header('Location: index.php' . (isset($_GET['page']) ? '?page=' . intval($_GET['page']) : '')); exit; } } if (isset($_SESSION['success_msg'])) { $success = $_SESSION['success_msg']; unset($_SESSION['success_msg']); } if (isset($_SESSION['error_msg'])) { $error = $_SESSION['error_msg']; unset($_SESSION['error_msg']); } $currentPage = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1; $perPage = 10; $kamiGuide = $db->getContent('kami_guide'); $successPopup = $db->getContent('success_popup'); $allKeys = $db->getAllKamiKeys(); $allLinks = $db->getAllLinks(); $usedKeys = array_filter($allKeys, function($k) { return ($k['use_count'] ?? 0) > 0; }); $availableKeys = array_filter($allKeys, function($k) { return ($k['use_count'] ?? 0) === 0; }); $permLinks = array_filter($allLinks, function($l) { return $l['is_permanent'] ?? false; }); $tempLinks = array_filter($allLinks, function($l) { return !($l['is_permanent'] ?? false); }); $totalKeys = $db->getKamiKeysCount(); $totalPages = max(1, ceil($totalKeys / $perPage)); $keys = $db->getKamiKeysPaginated($currentPage, $perPage); ?> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="anti-token" content="<?= AntiDuplicate::generateToken('global') ?>"> <title>管理后台 - QQ卡密系统</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%); min-height: 100vh; padding: 20px; } .container { max-width: 1100px; margin: 0 auto; } .card { background: rgba(255,255,255,0.05); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid rgba(255,255,255,0.1); border-radius: 24px; padding: 28px; margin-bottom: 20px; box-shadow: 0 8px 32px 0 rgba(0,0,0,0.37); } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; } .header h1 { color: white; font-size: 28px; font-weight: 700; } .form-group { margin-bottom: 20px; } .form-label { display: block; color: rgba(255,255,255,0.8); font-size: 14px; margin-bottom: 10px; font-weight: 500; } .form-input, .form-textarea { width: 100%; padding: 14px 16px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; font-size: 15px; color: white; outline: none; transition: all 0.3s; } .form-textarea { min-height: 120px; resize: vertical; } .form-input:focus, .form-textarea:focus { border-color: rgba(102,126,234,0.6); background: rgba(255,255,255,0.08); } .btn { padding: 14px 32px; background: linear-gradient(135deg, rgba(102,126,234,0.8) 0%, rgba(118,75,162,0.8) 100%); border: none; color: white; border-radius: 12px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.3s; } .btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(102,126,234,0.4); } .btn:disabled { opacity: 0.6; cursor: not-allowed; } .btn-danger { background: linear-gradient(135deg, rgba(239,68,68,0.8) 0%, rgba(220,38,38,0.8) 100%); padding: 8px 16px; font-size: 14px; } .btn-copy { background: rgba(102,126,234,0.3); border: 1px solid rgba(102,126,234,0.4); padding: 6px 12px; border-radius: 8px; color: white; font-size: 13px; cursor: pointer; transition: all 0.3s; } .btn-copy:hover:not(:disabled) { background: rgba(102,126,234,0.5); border-color: rgba(102,126,234,0.6); } .btn-secondary { background: rgba(255,255,255,0.1); } .error-box { background: rgba(254,242,242,0.1); border: 1px solid rgba(254,202,202,0.3); color: rgba(248,113,113,1); padding: 14px; border-radius: 12px; margin-bottom: 20px; } .success-box { background: rgba(240,253,244,0.1); border: 1px solid rgba(134,239,172,0.3); color: rgba(16,185,129,1); padding: 14px; border-radius: 12px; margin-bottom: 20px; } table { width: 100%; border-collapse: collapse; margin-top: 16px; } th, td { padding: 14px 12px; text-align: left; border-bottom: 1px solid rgba(255,255,255,0.1); color: rgba(255,255,255,0.9); font-size: 14px; } th { color: rgba(255,255,255,0.6); font-weight: 600; } td.kami-cell { font-family: monospace; font-size: 13px; } .badge { display: inline-block; padding: 4px 10px; border-radius: 20px; font-size: 12px; font-weight: 500; } .badge-perm { background: rgba(34,197,94,0.2); color: rgba(34,197,94,1); } .badge-temp { background: rgba(251,191,36,0.2); color: rgba(251,191,36,1); } .badge-expired { background: rgba(239,68,68,0.2); color: rgba(239,68,68,1); } .tabs { display: flex; gap: 12px; margin-bottom: 24px; flex-wrap: wrap; } .tab { padding: 12px 24px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; color: rgba(255,255,255,0.6); cursor: pointer; transition: all 0.3s; } .tab:hover { background: rgba(255,255,255,0.1); } .tab.active { background: rgba(102,126,234,0.2); border-color: rgba(102,126,234,0.4); color: white; } .tab-content { display: none; } .tab-content.active { display: block; } .stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; } .stat-card { background: rgba(0,0,0,0.2); padding: 20px; border-radius: 16px; text-align: center; } .stat-value { font-size: 36px; font-weight: 800; color: white; margin-bottom: 8px; } .stat-label { color: rgba(255,255,255,0.6); font-size: 14px; } .back-link { color: rgba(255,255,255,0.7); text-decoration: none; padding: 10px 16px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; display: inline-block; transition: all 0.3s; } .back-link:hover { color: white; background: rgba(255,255,255,0.1); } .actions-cell { display: flex; gap: 8px; align-items: center; } .pagination { display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 24px; flex-wrap: wrap; } .pagination-btn { padding: 8px 16px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 10px; color: white; font-size: 14px; cursor: pointer; transition: all 0.3s; } .pagination-btn:hover:not(.disabled) { background: rgba(102,126,234,0.2); border-color: rgba(102,126,234,0.4); } .pagination-btn.active { background: rgba(102,126,234,0.3); border-color: rgba(102,126,234,0.5); } .pagination-btn.disabled { opacity: 0.4; cursor: not-allowed; } .section-title { color: white; margin-bottom: 16px; font-size: 20px; font-weight: 600; } </style> </head> <body> <div class="container"> <div class="header"> <h1>🔐 管理后台</h1> <div style="display: flex; gap: 12px; flex-wrap: wrap;"> <a href="../index.php" class="back-link">← 返回首页</a> <?php if ($isLoggedIn): ?> <form method="POST" id="logout-form" data-anti-duplicate="true" style="display:inline;"> <input type="hidden" name="action" value="logout"> <input type="hidden" name="_anti_token" value="<?= AntiDuplicate::generateToken('logout') ?>"> <button type="submit" class="btn btn-secondary">退出登录</button> </form> <?php endif; ?> </div> </div> <?php if (!$isLoggedIn): ?> <div class="card"> <h2 style="color: white; margin-bottom: 24px; font-size: 20px;">管理员登录</h2> <?php if ($error): ?><div class="error-box"><?= htmlspecialchars($error) ?></div><?php endif; ?> <form method="POST" id="login-form" data-anti-duplicate="true"> <input type="hidden" name="action" value="login"> <input type="hidden" name="_anti_token" value="<?= AntiDuplicate::generateToken('login') ?>"> <div class="form-group"> <label class="form-label">用户名</label> <input type="text" class="form-input" name="username" placeholder="请输入用户名" required> </div> <div class="form-group"> <label class="form-label">密码</label> <input type="password" class="form-input" name="password" placeholder="请输入密码" required> </div> <button type="submit" class="btn">登录</button> </form> <p style="color: rgba(255,255,255,0.4); font-size: 12px; margin-top: 16px;">首次使用请前往安装页面设置账号密码</p> </div> <?php else: ?> <?php if ($error): ?><div class="error-box"><?= htmlspecialchars($error) ?></div><?php endif; ?> <?php if ($success): ?><div class="success-box"><?= htmlspecialchars($success) ?></div><?php endif; ?> <div class="tabs"> <div class="tab active" onclick="switchTab('dashboard')">首页</div> <div class="tab" onclick="switchTab('keys')">卡密管理</div> <div class="tab" onclick="switchTab('links')">链接管理</div> <div class="tab" onclick="switchTab('content')">内容管理</div> </div> <div class="tab-content active" id="tab-dashboard"> <div class="card"> <div class="stat-grid"> <div class="stat-card"> <div class="stat-value"><?= count($allKeys) ?></div> <div class="stat-label">总卡密数</div> </div> <div class="stat-card"> <div class="stat-value"><?= count($availableKeys) ?></div> <div class="stat-label">可用卡密</div> </div> <div class="stat-card"> <div class="stat-value"><?= count($allLinks) ?></div> <div class="stat-label">总链接数</div> </div> <div class="stat-card"> <div class="stat-value"><?= count($permLinks) ?></div> <div class="stat-label">永久链接</div> </div> </div> <h3 class="section-title">快速操作</h3> <div style="display: flex; gap: 12px; flex-wrap: wrap;"> <form method="POST" id="generate-form" data-anti-duplicate="true" style="display: flex; gap: 12px; align-items: center; margin: 0;"> <input type="hidden" name="action" value="generate_keys"> <input type="hidden" name="_anti_token" value="<?= AntiDuplicate::generateToken('generate_keys') ?>"> <input type="number" class="form-input" name="count" value="5" min="1" max="100" style="width: 120px;"> <button type="submit" class="btn">生成卡密</button> </form> <form method="POST" id="cleanup-form" data-anti-duplicate="true" style="margin: 0;"> <input type="hidden" name="action" value="cleanup"> <input type="hidden" name="_anti_token" value="<?= AntiDuplicate::generateToken('cleanup') ?>"> <button type="submit" class="btn btn-danger" style="padding: 14px 24px;">清理过期临时链接</button> </form> </div> </div> </div> <div class="tab-content" id="tab-keys"> <div class="card"> <h2 class="section-title">卡密列表</h2> <?php if (empty($keys)): ?> <p style="color: rgba(255,255,255,0.5);">暂无卡密,请先生成!</p> <?php else: ?> <table> <thead> <tr> <th>卡密</th> <th>使用次数</th> <th>创建时间</th> <th style="width: 100px;">操作</th> </tr> </thead> <tbody> <?php foreach ($keys as $key): ?> <tr> <td class="kami-cell"><?= htmlspecialchars($key['kami_key'] ?? '') ?></td> <td> <span class="badge" style="background: rgba(34,197,94,0.2); color: rgba(34,197,94,1);"> <?= $key['use_count'] ?? 0 ?>次 </span> </td> <td><?= htmlspecialchars($key['created_time'] ?? '') ?></td> <td> <button class="btn-copy" onclick="customCopy('<?= htmlspecialchars($key['kami_key'] ?? '') ?>')">复制</button> </td> </tr> <?php endforeach; ?> </tbody> </table> <?php if ($totalPages > 1): ?> <div class="pagination"> <?php if ($currentPage > 1): ?> <button class="pagination-btn" onclick="goToPage(1)">首页</button> <button class="pagination-btn" onclick="goToPage(<?= $currentPage - 1 ?>)">上一页</button> <?php else: ?> <button class="pagination-btn disabled">首页</button> <button class="pagination-btn disabled">上一页</button> <?php endif; ?> <?php $startPage = max(1, $currentPage - 2); $endPage = min($totalPages, $currentPage + 2); for ($i = $startPage; $i <= $endPage; $i++): ?> <button class="pagination-btn <?= $i === $currentPage ? 'active' : '' ?>" onclick="goToPage(<?= $i ?>)"><?= $i ?></button> <?php endfor; ?> <?php if ($currentPage < $totalPages): ?> <button class="pagination-btn" onclick="goToPage(<?= $currentPage + 1 ?>)">下一页</button> <button class="pagination-btn" onclick="goToPage(<?= $totalPages ?>)">末页</button> <?php else: ?> <button class="pagination-btn disabled">下一页</button> <button class="pagination-btn disabled">末页</button> <?php endif; ?> <span style="color: rgba(255,255,255,0.5); margin-left: 8px;"> 共 <?= $totalKeys ?> 条 / <?= $totalPages ?> 页 </span> </div> <?php endif; ?> <?php endif; ?> </div> </div> <div class="tab-content" id="tab-links"> <div class="card"> <h2 class="section-title">链接列表</h2> <?php if (empty($allLinks)): ?> <p style="color: rgba(255,255,255,0.5);">暂无链接!</p> <?php else: ?> <table> <thead> <tr> <th>链接ID</th> <th>标题</th> <th>类型</th> <th>创建时间</th> <th>过期时间</th> <th>操作</th> </tr> </thead> <tbody> <?php foreach ($allLinks as $link): ?> <tr> <td style="font-family: monospace;"><?= htmlspecialchars($link['link_id'] ?? '') ?></td> <td><?= htmlspecialchars($link['title'] ?? '') ?></td> <td> <?php if ($link['manually_expired'] ?? false): ?> <span class="badge badge-expired">已手动过期</span> <?php else: ?> <span class="badge <?= ($link['is_permanent'] ?? false) ? 'badge-perm' : 'badge-temp' ?>"> <?= ($link['is_permanent'] ?? false) ? '永久链接' : '临时链接' ?> </span> <?php endif; ?> </td> <td><?= htmlspecialchars($link['created_at'] ?? '') ?></td> <td> <?php if ($link['manually_expired'] ?? false): ?> <span style="color: rgba(239,68,68,1);">已过期</span> <?php elseif ($link['is_permanent'] ?? false): ?> 永不过期 <?php else: ?> <?= htmlspecialchars($link['expires_at'] ?? '') ?> <?php endif; ?> </td> <td class="actions-cell"> <?php if (!($link['manually_expired'] ?? false)): ?> <form method="POST" data-anti-duplicate="true" style="margin: 0;"> <input type="hidden" name="action" value="expire_link"> <input type="hidden" name="link_id" value="<?= htmlspecialchars($link['link_id']) ?>"> <input type="hidden" name="_anti_token" value="<?= AntiDuplicate::generateToken('expire_link') ?>"> <button type="submit" class="btn btn-danger" onclick="return confirm('确定要把这个链接设为过期吗?')">设为过期</button> </form> <?php else: ?> <span style="color: rgba(156,163,175,1);">已过期</span> <?php endif; ?> </td> </tr> <?php endforeach; ?> </tbody> </table> <?php endif; ?> </div> </div> <div class="tab-content" id="tab-content"> <div class="card"> <h2 class="section-title">内容管理</h2> <form method="POST" id="update-kami-form" data-anti-duplicate="true" style="margin-bottom: 32px;"> <input type="hidden" name="action" value="update_content"> <input type="hidden" name="content_key" value="kami_guide"> <input type="hidden" name="_anti_token" value="<?= AntiDuplicate::generateToken('update_content') ?>"> <div class="form-group"> <label class="form-label">卡密说明文本</label> <textarea class="form-textarea" name="content_value"><?= htmlspecialchars($kamiGuide ?? '') ?></textarea> </div> <button type="submit" class="btn">更新卡密说明</button> </form> <form method="POST" id="update-popup-form" data-anti-duplicate="true"> <input type="hidden" name="action" value="update_content"> <input type="hidden" name="content_key" value="success_popup"> <input type="hidden" name="_anti_token" value="<?= AntiDuplicate::generateToken('update_content') ?>"> <div class="form-group"> <label class="form-label">成功弹窗文本</label> <textarea class="form-textarea" name="content_value"><?= htmlspecialchars($successPopup ?? '') ?></textarea> </div> <button type="submit" class="btn">更新成功弹窗</button> </form> </div> </div> <?php endif; ?> </div> <script src="../assets/js/anti-duplicate.js"></script> <script> function switchTab(tab) { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); event.target.classList.add('active'); document.getElementById('tab-' + tab).classList.add('active'); } function goToPage(page) { window.location.href = 'index.php?page=' + page; } function showToast(message, type = 'success') { if (window.AntiDuplicate) { AntiDuplicate.showToast(message, type); } else { alert(message); } } function customCopy(text) { navigator.clipboard.writeText(text).then(() => { showToast('✅ 复制成功!', 'success'); }).catch(() => { const textarea = document.createElement('textarea'); textarea.value = text; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); showToast('✅ 复制成功!', 'success'); } catch (e) { showToast('❌ 复制失败,请手动复制', 'error'); } document.body.removeChild(textarea); }); } document.addEventListener('DOMContentLoaded', () => { const forms = document.querySelectorAll('form[data-anti-duplicate]'); forms.forEach(form => { AntiDuplicate.protectForm(form); }); }); </script> </body> </html>