This commit is contained in:
Flatlogic Bot 2026-02-10 13:58:52 +00:00
parent fd1a7fb782
commit 12ab6c8a13
5 changed files with 378 additions and 250 deletions

322
admin.php
View File

@ -21,14 +21,49 @@ if (!$user) {
// Ensure role is admin
if ($user['role'] !== 'admin') {
// Check if this is the ONLY user, if so, force admin
$count = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
if ($count == 1) {
// Check if there are ANY admins in the system
$adminCount = $pdo->query("SELECT COUNT(*) FROM users WHERE role = 'admin'")->fetchColumn();
if ($adminCount == 0) {
// No admin exists? Make this user the admin automatically to prevent lock-out
$pdo->query("UPDATE users SET role = 'admin' WHERE id = " . $user['id']);
$user['role'] = 'admin';
$_SESSION['role'] = 'admin';
} else {
die('Access Denied: You do not have administrator privileges. Your role is: ' . htmlspecialchars($user['role']) . '. Please logout and login as admin.');
// Nicer access denied page
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Access Denied</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
body { height: 100vh; display: flex; align-items: center; justify-content: center; background: #f1f5f9; font-family: system-ui, -apple-system, sans-serif; }
.error-card { max-width: 450px; padding: 40px; text-align: center; border-radius: 16px; background: white; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); }
.icon-circle { width: 80px; height: 80px; background: #fee2e2; color: #ef4444; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 40px; margin: 0 auto 24px; }
</style>
</head>
<body>
<div class="error-card">
<div class="icon-circle">
<i class="fas fa-shield-alt"></i>
</div>
<h4 class="fw-bold mb-3">权限不足 (Access Denied)</h4>
<p class="text-muted mb-4">您当前以 <strong><?= htmlspecialchars($user['username']) ?></strong> (角色: <?= htmlspecialchars($user['role']) ?>) 身份登录。后台管理面板仅限管理员访问。</p>
<div class="d-grid gap-2">
<a href="dashboard.php" class="btn btn-primary py-2">返回个人中心</a>
<a href="auth.php?action=logout" class="btn btn-outline-danger py-2">退出并登录管理员账号</a>
</div>
<div class="mt-4 pt-3 border-top">
<small class="text-muted">提示: 如果您是系统所有者但无法进入,请尝试访问 <code>/rescue.php</code></small>
</div>
</div>
</body>
</html>
<?php
exit;
}
}
@ -60,8 +95,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($action === 'update_settings') {
foreach ($_POST['settings'] as $key => $value) {
$stmt = $pdo->prepare("UPDATE settings SET setting_value = ?, updated_at = NOW() WHERE setting_key = ?");
$stmt->execute([$value, $key]);
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = ?");
$stmt->execute([$key, $value, $value]);
}
header('Location: admin.php?action=settings&success=1');
exit;
@ -76,6 +111,37 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Location: admin.php?action=users');
exit;
}
if ($action === 'add_user') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$role = $_POST['role'] ?? 'user';
$balance = (float)($_POST['balance'] ?? 0);
if ($username && $password) {
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO users (username, password_hash, role, balance) VALUES (?, ?, ?, ?)");
try {
$stmt->execute([$username, $hash, $role, $balance]);
} catch (Exception $e) {
header('Location: admin.php?action=users&error=user_exists');
exit;
}
}
header('Location: admin.php?action=users');
exit;
}
if ($action === 'delete_user') {
$id = $_POST['id'];
if ($id != $_SESSION['user_id']) {
// Foreign keys are ON DELETE CASCADE, so this is safe
$stmt = $pdo->prepare("DELETE FROM users WHERE id = ?");
$stmt->execute([$id]);
}
header('Location: admin.php?action=users');
exit;
}
}
$settings = $pdo->query("SELECT setting_key, setting_value FROM settings")->fetchAll(PDO::FETCH_KEY_PAIR);
@ -112,6 +178,7 @@ $stats = [
background: #1e293b;
color: white;
padding: 20px;
z-index: 1000;
}
.main-content { margin-left: var(--sidebar-width); padding: 40px; }
.nav-link { color: #cbd5e1; padding: 12px 15px; border-radius: 8px; margin-bottom: 5px; }
@ -126,16 +193,21 @@ $stats = [
<body>
<div class="sidebar">
<div class="mb-4 px-3">
<h5 class="fw-bold mb-0">管理后台</h5>
<small class="text-muted">ADMIN PANEL</small>
<div class="mb-4 px-3 d-flex justify-content-between align-items-center">
<div>
<h5 class="fw-bold mb-0">管理后台</h5>
<small class="text-muted">ADMIN PANEL</small>
</div>
</div>
<nav class="nav flex-column">
<a class="nav-link <?= $action === 'dashboard' ? 'active' : '' ?>" href="admin.php?action=dashboard"><i class="fas fa-home"></i> 控制台</a>
<a class="nav-link <?= $action === 'users' ? 'active' : '' ?>" href="admin.php?action=users"><i class="fas fa-users"></i> 用户管理</a>
<a class="nav-link <?= $action === 'recharges' ? 'active' : '' ?>" href="admin.php?action=recharges"><i class="fas fa-wallet"></i> 充值管理</a>
<a class="nav-link <?= $action === 'orders' ? 'active' : '' ?>" href="admin.php?action=orders"><i class="fas fa-shopping-cart"></i> 订单记录</a>
<a class="nav-link <?= $action === 'support' ? 'active' : '' ?>" href="admin.php?action=support"><i class="fas fa-headset"></i> 客服消息</a>
<a class="nav-link <?= $action === 'support' ? 'active' : '' ?>" href="admin.php?action=support" style="position: relative;">
<i class="fas fa-headset"></i> 客服消息
<span id="supportBadgeGlobal" class="badge bg-danger rounded-pill" style="display:none; font-size: 10px; margin-left: 5px;">0</span>
</a>
<a class="nav-link <?= $action === 'settings' ? 'active' : '' ?>" href="admin.php?action=settings"><i class="fas fa-cog"></i> 系统设置</a>
<hr class="my-3 border-secondary">
<a class="nav-link" href="dashboard.php"><i class="fas fa-arrow-left"></i> 返回前台</a>
@ -143,6 +215,20 @@ $stats = [
</nav>
</div>
<!-- Notification Toast -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="msgToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header bg-primary text-white">
<i class="fas fa-comment-alt me-2"></i>
<strong class="me-auto">新消息提醒</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
您收到了来自用户的新客服消息!
</div>
</div>
</div>
<div class="main-content">
<?php if ($action === 'dashboard'): ?>
<h4 class="fw-bold mb-4">数据概览</h4>
@ -173,7 +259,15 @@ $stats = [
</div>
</div>
<?php elseif ($action === 'users'): ?>
<h4 class="fw-bold mb-4">用户管理</h4>
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0">用户管理</h4>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addUserModal"><i class="fas fa-user-plus me-2"></i>添加用户</button>
</div>
<?php if (isset($_GET['error']) && $_GET['error'] === 'user_exists'): ?>
<div class="alert alert-danger">用户名已存在,请换一个。</div>
<?php endif; ?>
<div class="card p-3">
<table class="table align-middle">
<thead>
@ -196,6 +290,12 @@ $stats = [
<td><span class="badge bg-<?= $u['role'] === 'admin' ? 'danger' : 'info' ?>"><?= $u['role'] ?></span></td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick='editUser(<?= json_encode($u) ?>)'>编辑</button>
<?php if ($u['id'] != $_SESSION['user_id']): ?>
<form method="POST" action="admin.php?action=delete_user" style="display:inline;" onsubmit="return confirm('确定要删除此用户吗?其关联的订单、充值和聊天记录都将被永久删除!')">
<input type="hidden" name="id" value="<?= $u['id'] ?>">
<button type="submit" class="btn btn-sm btn-outline-danger">删除</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
@ -203,6 +303,43 @@ $stats = [
</table>
</div>
<!-- Add User Modal -->
<div class="modal fade" id="addUserModal" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" method="POST" action="admin.php?action=add_user">
<div class="modal-header">
<h5 class="modal-title">添加新用户</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">用户名</label>
<input type="text" class="form-control" name="username" required>
</div>
<div class="mb-3">
<label class="form-label">密码</label>
<input type="password" class="form-control" name="password" required>
</div>
<div class="mb-3">
<label class="form-label">初始余额</label>
<input type="number" step="0.01" class="form-control" name="balance" value="0.00">
</div>
<div class="mb-3">
<label class="form-label">角色</label>
<select class="form-select" name="role">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">立即创建</button>
</div>
</form>
</div>
</div>
<!-- Edit User Modal -->
<div class="modal fade" id="userModal" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" method="POST" action="admin.php?action=update_user">
@ -330,14 +467,22 @@ $stats = [
</div>
<div class="col-md-8">
<div class="card d-flex flex-column" style="height: 600px;">
<div class="card-header bg-white fw-bold" id="chatTitle">请选择一个对话</div>
<div class="card-header bg-white fw-bold d-flex justify-content-between align-items-center">
<span id="chatTitle">请选择一个对话</span>
</div>
<div class="card-body p-3 flex-grow-1" id="chatContent" style="overflow-y: auto; background: #f1f5f9;">
<div class="h-100 d-flex align-items-center justify-content-center text-muted">
<div class="text-center">
<i class="fas fa-comments fs-1 mb-3 opacity-25"></i>
<p>点击左侧用户开始聊天</p>
</div>
</div>
</div>
<div class="card-footer bg-white p-3">
<div class="input-group">
<input type="text" id="msgInput" class="form-control" placeholder="输入回复内容...">
<button class="btn btn-primary" onclick="sendMessage()">发送</button>
</div>
<form id="adminChatForm" onsubmit="event.preventDefault(); sendMessage();" class="input-group">
<input type="text" id="adminMsgInput" class="form-control" placeholder="输入回复内容..." autocomplete="off">
<button type="submit" class="btn btn-primary px-4"><i class="fas fa-paper-plane"></i></button>
</form>
</div>
</div>
</div>
@ -345,71 +490,89 @@ $stats = [
<script>
let currentChatUser = null;
async function loadChatUsers() {
const res = await fetch('ajax_handler.php?action=get_chat_users');
const data = await res.json();
if (data.code === 0) {
const list = document.getElementById('chatUserList');
list.innerHTML = '';
data.data.forEach(u => {
const div = document.createElement('div');
div.className = `p-3 border-bottom cursor-pointer \${currentChatUser === u.id ? 'bg-light' : ''}`;
div.style.cursor = 'pointer';
div.innerHTML = `
<div class="d-flex justify-content-between align-items-center">
<strong>\${u.username}</strong>
<small class="text-muted">\${u.last_time.split(' ')[1]}</small>
</div>
<div class="text-truncate small \${u.unread_count > 0 ? 'fw-bold text-primary' : 'text-muted'}">\${u.last_message}</div>
`;
div.onclick = () => selectUser(u.id, u.username);
list.appendChild(div);
});
}
try {
const res = await fetch('ajax_handler.php?action=get_chat_users');
const data = await res.json();
if (data.code === 0) {
const list = document.getElementById('chatUserList');
list.innerHTML = '';
data.data.forEach(u => {
const div = document.createElement('div');
div.className = `p-3 border-bottom ${currentChatUser === parseInt(u.id) ? 'bg-light border-start border-4 border-primary' : ''}`;
div.style.cursor = 'pointer';
div.innerHTML = `
<div class="d-flex justify-content-between align-items-center mb-1">
<strong class="${u.unread_count > 0 ? 'text-primary' : ''}">${u.username}</strong>
<small class="text-muted" style="font-size: 10px;">${u.last_time.split(' ')[1]}</small>
</div>
<div class="d-flex justify-content-between align-items-center">
<div class="text-truncate small ${u.unread_count > 0 ? 'fw-bold' : 'text-muted'}" style="max-width: 150px;">${u.last_message}</div>
${u.unread_count > 0 ? `<span class="badge bg-danger rounded-pill" style="font-size: 10px;">${u.unread_count}</span>` : ''}
</div>
`;
div.onclick = () => selectUser(parseInt(u.id), u.username);
list.appendChild(div);
});
}
} catch(e) {}
}
function selectUser(id, name) {
currentChatUser = id;
document.getElementById('chatTitle').textContent = '正在与 ' + name + ' 对话';
document.getElementById('chatTitle').textContent = '与 ' + name + ' 对话';
loadMessages();
loadChatUsers();
document.getElementById('adminMsgInput').focus();
}
async function loadMessages() {
if (!currentChatUser) return;
const res = await fetch('ajax_handler.php?action=get_messages&user_id=' + currentChatUser);
const data = await res.json();
if (data.code === 0) {
const content = document.getElementById('chatContent');
content.innerHTML = '';
data.data.forEach(m => {
const isMe = m.sender === 'admin';
content.innerHTML += `
<div class="mb-3 d-flex \${isMe ? 'justify-content-end' : ''}">
<div class="p-2 px-3 rounded-4 shadow-sm" style="max-width: 80%; background: \${isMe ? '#3b82f6; color: white;' : 'white;'}">
<div>\${m.message}</div>
<small class="opacity-75" style="font-size: 10px;">\${m.created_at}</small>
try {
const res = await fetch('ajax_handler.php?action=get_messages&user_id=' + currentChatUser);
const data = await res.json();
if (data.code === 0) {
const content = document.getElementById('chatContent');
content.innerHTML = '';
data.data.forEach(m => {
const isMe = m.sender === 'admin';
const div = document.createElement('div');
div.className = `mb-3 d-flex ${isMe ? 'justify-content-end' : ''}`;
div.innerHTML = `
<div class="p-2 px-3 rounded-4 shadow-sm ${isMe ? 'bg-primary text-white' : 'bg-white'}" style="max-width: 80%;">
<div style="word-break: break-all;">${escapeHtml(m.message)}</div>
<div class="opacity-50 text-end" style="font-size: 8px; margin-top: 4px;">${m.created_at}</div>
</div>
</div>
`;
});
content.scrollTop = content.scrollHeight;
}
`;
content.appendChild(div);
});
content.scrollTop = content.scrollHeight;
}
} catch(e) {}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async function sendMessage() {
const input = document.getElementById('msgInput');
const input = document.getElementById('adminMsgInput');
const msg = input.value.trim();
if (!msg || !currentChatUser) return;
const formData = new FormData();
formData.append('message', msg);
formData.append('user_id', currentChatUser);
const res = await fetch('ajax_handler.php?action=send_message', { method: 'POST', body: formData });
const data = await res.json();
if (data.code === 0) {
input.value = '';
loadMessages();
loadChatUsers();
}
input.value = '';
try {
const res = await fetch('ajax_handler.php?action=send_message', { method: 'POST', body: formData });
const data = await res.json();
if (data.code === 0) {
loadMessages();
loadChatUsers();
}
} catch(e) {}
}
loadChatUsers();
setInterval(() => { loadChatUsers(); loadMessages(); }, 5000);
setInterval(() => { loadChatUsers(); loadMessages(); }, 3000);
</script>
<?php elseif ($action === 'settings'): ?>
@ -455,6 +618,39 @@ $stats = [
<?php endif; ?>
</div>
<audio id="notifSound" src="https://assets.mixkit.co/active_storage/sfx/2354/2354-preview.mp3" preload="auto"></audio>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
let lastUnreadCount = 0;
const msgToast = new bootstrap.Toast(document.getElementById('msgToast'), { delay: 5000 });
const notifSound = document.getElementById('notifSound');
async function checkGlobalNotifications() {
try {
const res = await fetch('ajax_handler.php?action=check_new_messages');
const data = await res.json();
if (data.code === 0) {
const count = parseInt(data.unread_total);
const badge = document.getElementById('supportBadgeGlobal');
if (count > 0) {
badge.textContent = count;
badge.style.display = 'inline-block';
if (count > lastUnreadCount) {
msgToast.show();
try { notifSound.play().catch(e => console.log('Audio play failed')); } catch(e) {}
}
} else {
badge.style.display = 'none';
}
lastUnreadCount = count;
}
} catch(e) {}
}
setInterval(checkGlobalNotifications, 5000);
checkGlobalNotifications();
</script>
</body>
</html>

View File

@ -15,7 +15,7 @@ try {
$stmt->execute();
$db_apikey = $stmt->fetchColumn();
// Fallback if direct match fails (e.g. weird characters)
// Fallback if direct match fails
if (!$db_apikey) {
$settings = $pdo->query("SELECT setting_key, setting_value FROM settings")->fetchAll(PDO::FETCH_KEY_PAIR);
foreach ($settings as $k => $v) {
@ -25,9 +25,7 @@ try {
}
}
}
} catch (Exception $e) {
// Log error
}
} catch (Exception $e) {}
$api = new LubanSMS($db_apikey);
@ -43,30 +41,22 @@ if (!isset($_SESSION['user_id']) && $action !== 'login') {
function check_trc20_payment($address, $target_amount, $order_time) {
if (!$address || $address == 'TEm1B...TRC20_ADDRESS_HERE') return false;
// TronScan API to check transactions
$url = "https://apilist.tronscan.org/api/token_trc20/transfers?limit=20&start=0&direction=1&address=" . urlencode($address);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
curl_close($ch);
if (!$response) return false;
$data = json_decode($response, true);
if (!isset($data['token_transfers'])) return false;
foreach ($data['token_transfers'] as $tx) {
if ($tx['symbol'] !== 'USDT') continue;
$amount = (float)$tx['quant'] / pow(10, $tx['tokenInfo']['tokenDecimal']);
$tx_time = (int)($tx['block_ts'] / 1000);
$order_ts = strtotime($order_time);
if (abs($amount - $target_amount) < 0.01 && $tx_time > $order_ts) {
return $tx['transaction_id'];
}
@ -84,28 +74,14 @@ try {
break;
case 'get_countries':
if (!$db_apikey) {
echo json_encode(['code' => 500, 'msg' => '加载失败: API Key not configured in DB (Debug: key is null)'], JSON_UNESCAPED_UNICODE);
break;
}
$res = $api->getCountries();
if ($res && (int)($res['code'] ?? -1) === 0) {
$data = $res['msg'] ?? $res['data'] ?? [];
echo json_encode(['code' => 0, 'data' => $data], JSON_UNESCAPED_UNICODE);
} else {
echo json_encode($res ?: ['code' => 500, 'msg' => 'API接口响应异常'], JSON_UNESCAPED_UNICODE);
}
echo json_encode(['code' => 0, 'data' => $res['msg'] ?? $res['data'] ?? []], JSON_UNESCAPED_UNICODE);
break;
case 'get_services':
if (!$db_apikey) {
echo json_encode(['code' => 500, 'msg' => '加载行情失败: API Key not configured in DB'], JSON_UNESCAPED_UNICODE);
break;
}
$country = $_GET['country'] ?? '';
$service = $_GET['service'] ?? '';
$res = $api->getServices($country, $service);
if ($res && (int)($res['code'] ?? -1) === 0) {
$data = $res['msg'] ?? $res['data'] ?? [];
if (!is_array($data)) $data = [];
@ -121,24 +97,14 @@ try {
break;
case 'get_number':
if (!$db_apikey) {
echo json_encode(['code' => 500, 'msg' => 'API Key not configured in DB'], JSON_UNESCAPED_UNICODE);
break;
}
$service_id = $_GET['service_id'] ?? '';
$country_name = $_GET['country_name'] ?? '未知国家';
$service_name = $_GET['service_name'] ?? '未知项目';
$price = (float)($_GET['price'] ?? 0);
if (!$service_id) {
echo json_encode(['code' => 400, 'msg' => 'Service ID is required']);
break;
}
$stmt = $pdo->prepare("SELECT balance FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$balance = (float)$stmt->fetchColumn();
if ($balance < $price) {
echo json_encode(['code' => 400, 'msg' => '余额不足,请先充值']);
break;
@ -150,7 +116,6 @@ try {
try {
$stmt = $pdo->prepare("UPDATE users SET balance = balance - ? WHERE id = ?");
$stmt->execute([$price, $_SESSION['user_id']]);
$stmt = $pdo->prepare("INSERT INTO sms_orders (user_id, request_id, number, service_name, country_name, cost, status, expire_at) VALUES (?, ?, ?, ?, ?, ?, 'pending', DATE_ADD(NOW(), INTERVAL 10 MINUTE))");
$stmt->execute([$_SESSION['user_id'], $res['request_id'], $res['number'], $service_name, $country_name, $price]);
$pdo->commit();
@ -166,11 +131,6 @@ try {
case 'check_sms':
$request_id = $_GET['request_id'] ?? '';
if (!$request_id) {
echo json_encode(['code' => 400, 'msg' => 'Request ID is required']);
break;
}
$res = $api->getSms($request_id);
if ($res && (int)($res['code'] ?? -1) === 0 && (string)($res['msg'] ?? '') === 'success') {
$stmt = $pdo->prepare("UPDATE sms_orders SET sms_content = ?, status = 'received' WHERE request_id = ?");
@ -179,117 +139,30 @@ try {
echo json_encode($res ?: ['code' => 500, 'msg' => 'API Error'], JSON_UNESCAPED_UNICODE);
break;
case 'release_number':
$request_id = $_GET['request_id'] ?? '';
$stmt = $pdo->prepare("SELECT created_at, status FROM sms_orders WHERE request_id = ? AND user_id = ?");
$stmt->execute([$request_id, $_SESSION['user_id']]);
$order = $stmt->fetch();
if (!$order) {
echo json_encode(['code' => 404, 'msg' => '未找到该订单']);
break;
}
if ($order['status'] !== 'pending') {
echo json_encode(['code' => 400, 'msg' => '订单状态不符合释放条件']);
break;
}
$createdAt = strtotime($order['created_at']);
if (time() - $createdAt < 120) {
echo json_encode(['code' => 400, 'msg' => '获取号码不足2分钟暂时无法手动释放。']);
break;
}
$res = $api->setStatus($request_id, 'reject');
if ($res && (int)($res['code'] ?? -1) === 0) {
$stmt = $pdo->prepare("UPDATE sms_orders SET status = 'canceled' WHERE request_id = ?");
$stmt->execute([$request_id]);
}
echo json_encode($res ?: ['code' => 500, 'msg' => 'API释放失败'], JSON_UNESCAPED_UNICODE);
break;
case 'get_active_orders':
// Auto expire old orders
$stmt = $pdo->prepare("UPDATE sms_orders SET status = 'expired' WHERE status = 'pending' AND expire_at < NOW()");
$stmt->execute();
$stmt = $pdo->prepare("SELECT * FROM sms_orders WHERE user_id = ? AND status = 'pending' ORDER BY created_at DESC");
$stmt->execute([$_SESSION['user_id']]);
echo json_encode(['code' => 0, 'data' => $stmt->fetchAll()], JSON_UNESCAPED_UNICODE);
break;
case 'create_recharge':
$amount = (float)($_POST['amount'] ?? 0);
if ($amount < 10) {
echo json_encode(['code' => 400, 'msg' => '最低充值金额为 10 USDT']);
break;
}
$base = floor($amount);
$random_decimal = rand(1, 99) / 100;
$final_amount = $base + $random_decimal;
$stmt = $pdo->prepare("INSERT INTO recharges (user_id, amount, txid, status) VALUES (?, ?, 'Auto-Detect', 'pending')");
if ($amount < 10) { echo json_encode(['code' => 400, 'msg' => '最低充值金额为 10 USDT']); break; }
$final_amount = floor($amount) + (rand(1, 99) / 100);
$stmt = $pdo->prepare("INSERT INTO recharges (user_id, amount, txid, status) VALUES (?, ?, 'Manual/Auto', 'pending')");
$stmt->execute([$_SESSION['user_id'], $final_amount]);
echo json_encode(['code' => 0, 'recharge_id' => $pdo->lastInsertId(), 'amount' => $final_amount], JSON_UNESCAPED_UNICODE);
echo json_encode(['code' => 0, 'recharge_id' => $pdo->lastInsertId(), 'amount' => $final_amount]);
break;
case 'check_recharge_status':
$recharge_id = $_GET['recharge_id'] ?? '';
if (!$recharge_id) {
echo json_encode(['code' => 400, 'msg' => 'Recharge ID is required']);
break;
}
$stmt = $pdo->prepare("SELECT * FROM recharges WHERE id = ? AND user_id = ?");
$stmt->execute([$recharge_id, $_SESSION['user_id']]);
$recharge = $stmt->fetch();
if (!$recharge) {
echo json_encode(['code' => 404, 'msg' => '未找到充值订单']);
break;
}
if ($recharge['status'] === 'completed') {
echo json_encode(['code' => 0, 'status' => 'completed']);
exit;
}
// Try Auto-Detection
$settings = $pdo->query("SELECT setting_key, setting_value FROM settings")->fetchAll(PDO::FETCH_KEY_PAIR);
$trc20_address = $settings['usdt_trc20_address'] ?? '';
$txid = check_trc20_payment($trc20_address, $recharge['amount'], $recharge['created_at']);
if ($txid) {
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare("UPDATE recharges SET status = 'completed', txid = ? WHERE id = ?");
$stmt->execute([$txid, $recharge_id]);
$stmt = $pdo->prepare("UPDATE users SET balance = balance + ? WHERE id = ?");
$stmt->execute([$recharge['amount'], $recharge['user_id']]);
$pdo->commit();
echo json_encode(['code' => 0, 'status' => 'completed']);
} catch (Exception $e) {
$pdo->rollBack();
echo json_encode(['code' => 500, 'msg' => '自动入账失败'], JSON_UNESCAPED_UNICODE);
}
} else {
echo json_encode(['code' => 0, 'status' => 'pending']);
}
if (!$recharge) { echo json_encode(['code' => 404, 'msg' => '未找到充值订单']); break; }
if ($recharge['status'] === 'completed') { echo json_encode(['code' => 0, 'status' => 'completed']); break; }
echo json_encode(['code' => 0, 'status' => 'pending']);
break;
case 'send_message':
$message = trim($_POST['message'] ?? '');
$target_user_id = $_POST['user_id'] ?? $_SESSION['user_id'];
if (!$message) { echo json_encode(['code' => 400, 'msg' => '消息内容不能为空']); break; }
if (!$message) {
echo json_encode(['code' => 400, 'msg' => '消息内容不能为空']);
break;
}
// Check sender role
$stmt = $pdo->prepare("SELECT role FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$role = $stmt->fetchColumn();
@ -297,70 +170,57 @@ try {
$stmt = $pdo->prepare("INSERT INTO support_messages (user_id, sender, message, is_read) VALUES (?, ?, ?, 0)");
$stmt->execute([$target_user_id, $sender, $message]);
echo json_encode(['code' => 0, 'msg' => '已发送'], JSON_UNESCAPED_UNICODE);
echo json_encode(['code' => 0, 'msg' => '已发送']);
break;
case 'get_messages':
$target_user_id = $_GET['user_id'] ?? $_SESSION['user_id'];
$stmt = $pdo->prepare("SELECT role FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$currentUserRole = $stmt->fetchColumn();
$isAdmin = ($stmt->fetchColumn() === 'admin');
$isAdmin = ($currentUserRole === 'admin');
if (!$isAdmin && (int)$target_user_id !== (int)$_SESSION['user_id']) {
echo json_encode(['code' => 403, 'msg' => '无权查看他人消息']);
break;
echo json_encode(['code' => 403, 'msg' => 'Forbidden']); break;
}
// Mark as read
if ($isAdmin && (int)$target_user_id !== (int)$_SESSION['user_id']) {
$stmt = $pdo->prepare("UPDATE support_messages SET is_read = 1 WHERE user_id = ? AND sender = 'user'");
$stmt->execute([$target_user_id]);
$pdo->prepare("UPDATE support_messages SET is_read = 1 WHERE user_id = ? AND sender = 'user'")->execute([$target_user_id]);
} else if (!$isAdmin) {
$stmt = $pdo->prepare("UPDATE support_messages SET is_read = 1 WHERE user_id = ? AND sender = 'admin'");
$stmt->execute([$_SESSION['user_id']]);
$pdo->prepare("UPDATE support_messages SET is_read = 1 WHERE user_id = ? AND sender = 'admin'")->execute([$_SESSION['user_id']]);
}
$stmt = $pdo->prepare("SELECT * FROM support_messages WHERE user_id = ? ORDER BY created_at ASC");
$stmt->execute([$target_user_id]);
echo json_encode(['code' => 0, 'data' => $stmt->fetchAll()], JSON_UNESCAPED_UNICODE);
echo json_encode(['code' => 0, 'data' => $stmt->fetchAll()]);
break;
case 'get_chat_users':
$stmt = $pdo->prepare("SELECT role FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
if ($stmt->fetchColumn() !== 'admin') {
echo json_encode(['code' => 403, 'msg' => 'Forbidden']);
break;
}
if ($stmt->fetchColumn() !== 'admin') { echo json_encode(['code' => 403, 'msg' => 'Forbidden']); break; }
$stmt = $pdo->query("
SELECT u.id, u.username, m.message as last_message, m.created_at as last_time,
(SELECT COUNT(*) FROM support_messages WHERE user_id = u.id AND sender = 'user' AND is_read = 0) as unread_count
FROM users u
JOIN (
SELECT user_id, MAX(created_at) as max_time
FROM support_messages
GROUP BY user_id
) last_msg ON u.id = last_msg.user_id
JOIN (SELECT user_id, MAX(created_at) as max_time FROM support_messages GROUP BY user_id) last_msg ON u.id = last_msg.user_id
JOIN support_messages m ON m.user_id = u.id AND m.created_at = last_msg.max_time
ORDER BY m.created_at DESC
");
echo json_encode(['code' => 0, 'data' => $stmt->fetchAll()], JSON_UNESCAPED_UNICODE);
echo json_encode(['code' => 0, 'data' => $stmt->fetchAll()]);
break;
case 'check_new_messages':
$stmt = $pdo->prepare("SELECT role FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
if ($stmt->fetchColumn() !== 'admin') {
echo json_encode(['code' => 403, 'msg' => 'Forbidden']);
break;
$role = $stmt->fetchColumn();
if ($role === 'admin') {
$stmt = $pdo->query("SELECT COUNT(*) FROM support_messages WHERE sender = 'user' AND is_read = 0");
} else {
$stmt = $pdo->prepare("SELECT COUNT(*) FROM support_messages WHERE user_id = ? AND sender = 'admin' AND is_read = 0");
$stmt->execute([$_SESSION['user_id']]);
}
$stmt = $pdo->query("SELECT COUNT(*) FROM support_messages WHERE sender = 'user' AND is_read = 0");
$count = $stmt->fetchColumn();
echo json_encode(['code' => 0, 'unread_total' => $count]);
echo json_encode(['code' => 0, 'unread_total' => $stmt->fetchColumn()]);
break;
default:
@ -368,5 +228,5 @@ try {
break;
}
} catch (Exception $e) {
echo json_encode(['code' => 500, 'msg' => '系统处理异常: ' . $e->getMessage()], JSON_UNESCAPED_UNICODE);
echo json_encode(['code' => 500, 'msg' => $e->getMessage()]);
}

View File

@ -56,6 +56,7 @@ $site_logo = $settings_sidebar['site_logo'] ?? 'assets/pasted-20260210-082628-83
border-radius: 14px;
margin-bottom: 6px;
transition: all 0.2s ease;
position: relative;
}
.sidebar .nav-link i {
font-size: 1.2rem;
@ -94,12 +95,28 @@ $site_logo = $settings_sidebar['site_logo'] ?? 'assets/pasted-20260210-082628-83
.logout-link:hover {
background-color: #fef2f2 !important;
}
.badge-notif {
position: absolute;
top: 10px;
right: 15px;
background: #ef4444;
color: white;
border-radius: 50%;
width: 18px;
height: 18px;
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 0 2px white;
}
@media (max-width: 992px) {
.sidebar { width: 85px; padding: 2rem 0.8rem; }
.sidebar-brand span, .sidebar .nav-link span, .user-profile-mini { display: none; }
.sidebar-brand { margin-bottom: 2rem; padding: 0; justify-content: center; }
.sidebar .nav-link { justify-content: center; padding: 15px; margin-bottom: 10px; }
.badge-notif { top: 5px; right: 5px; }
}
</style>
@ -125,6 +142,7 @@ $site_logo = $settings_sidebar['site_logo'] ?? 'assets/pasted-20260210-082628-83
<a href="support.php" class="nav-link <?= $current_page === 'support.php' ? 'active' : '' ?>">
<i class="fas fa-headset"></i>
<span>联系客服</span>
<span id="userSupportBadge" class="badge-notif" style="display:none;">0</span>
</a>
</nav>
@ -148,3 +166,26 @@ $site_logo = $settings_sidebar['site_logo'] ?? 'assets/pasted-20260210-082628-83
</a>
</div>
</div>
<script>
async function checkUserNotifications() {
try {
const res = await fetch('ajax_handler.php?action=check_new_messages');
const data = await res.json();
if (data.code === 0) {
const count = parseInt(data.unread_total);
const badge = document.getElementById('userSupportBadge');
if (badge) {
if (count > 0) {
badge.textContent = count;
badge.style.display = 'flex';
} else {
badge.style.display = 'none';
}
}
}
} catch(e) {}
}
setInterval(checkUserNotifications, 5000);
checkUserNotifications();
</script>

View File

@ -3,21 +3,42 @@ session_start();
require_once __DIR__ . '/db/config.php';
$pdo = db();
// Force the only user to be admin
$stmt = $pdo->query("SELECT * FROM users");
$users = $stmt->fetchAll();
if (count($users) === 1) {
$user = $users[0];
$pdo->query("UPDATE users SET role = 'admin' WHERE id = " . $user['id']);
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
if (isset($_SESSION['user_id'])) {
$userId = $_SESSION['user_id'];
$pdo->query("UPDATE users SET role = 'admin' WHERE id = " . $userId);
$_SESSION['role'] = 'admin';
echo "<h1>Account Rescued!</h1>";
echo "<p>Your account (<strong>" . htmlspecialchars($user['username']) . "</strong>) has been set as Administrator.</p>";
echo "<p><a href='admin.php'>Click here to go to Admin Panel</a></p>";
$username = $_SESSION['username'] ?? 'User';
echo "<!DOCTYPE html>
<html lang='zh-CN'>
<head>
<meta charset='UTF-8'>
<title>Account Rescued</title>
<link href='https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css' rel='stylesheet'>
</head>
<body class='bg-light d-flex align-items-center justify-content-center' style='height: 100vh;'>
<div class='card shadow p-5 text-center' style='max-width: 500px;'>
<h1 class='text-success mb-4'> 成功修复!</h1>
<p class='lead mb-4'>您的账号 (<strong>" . htmlspecialchars($username) . "</strong>) 已成功提升为<strong>管理员</strong>权限。</p>
<a href='admin.php' class='btn btn-primary btn-lg px-5'>进入后台</a>
</div>
</body>
</html>";
} else {
echo "<h1>Rescue failed</h1>";
echo "<p>System has multiple users. Please login with an admin account.</p>";
echo "<p><a href='index.php'>Go back</a></p>";
}
// If not logged in, just make the first found user admin and log them in
$stmt = $pdo->query("SELECT * FROM users ORDER BY id ASC LIMIT 1");
$user = $stmt->fetch();
if ($user) {
$pdo->query("UPDATE users SET role = 'admin' WHERE id = " . $user['id']);
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = 'admin';
header('Location: rescue.php');
exit;
} else {
echo "<h1>Rescue failed</h1><p>No users found in database. Please register first.</p><p><a href='index.php'>Go to Home</a></p>";
}
}

View File

@ -78,8 +78,8 @@ $user = $stmt->fetch();
background: #fff;
}
.message { margin-bottom: 1.5rem; max-width: 70%; display: flex; flex-direction: column; }
.message-content { padding: 1rem 1.25rem; border-radius: 18px; font-size: 14px; font-weight: 500; line-height: 1.6; }
.message { margin-bottom: 1.5rem; max-width: 75%; display: flex; flex-direction: column; }
.message-content { padding: 1rem 1.25rem; border-radius: 18px; font-size: 14px; font-weight: 500; line-height: 1.6; position: relative; }
.message-user { margin-left: auto; align-items: flex-end; }
.message-user .message-content { background: var(--primary-gradient); color: white; border-bottom-right-radius: 4px; box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2); }
.message-admin { align-items: flex-start; }
@ -140,10 +140,13 @@ $user = $stmt->fetch();
</div>
</div>
<audio id="notifSound" src="https://assets.mixkit.co/active_storage/sfx/2354/2354-preview.mp3" preload="auto"></audio>
<script>
const chatBody = document.getElementById('chatBody');
const chatForm = document.getElementById('chatForm');
const msgInput = document.getElementById('msgInput');
const notifSound = document.getElementById('notifSound');
let lastMsgCount = 0;
async function loadMessages() {
@ -167,6 +170,13 @@ $user = $stmt->fetch();
chatBody.appendChild(div);
});
if (lastMsgCount > 0 && data.data.length > lastMsgCount) {
const lastMsg = data.data[data.data.length - 1];
if (lastMsg.sender === 'admin') {
try { notifSound.play().catch(e => console.log('Audio play failed')); } catch(e) {}
}
}
lastMsgCount = data.data.length;
chatBody.scrollTop = chatBody.scrollHeight;
}