232323
This commit is contained in:
parent
fd1a7fb782
commit
12ab6c8a13
322
admin.php
322
admin.php
@ -21,14 +21,49 @@ if (!$user) {
|
|||||||
|
|
||||||
// Ensure role is admin
|
// Ensure role is admin
|
||||||
if ($user['role'] !== 'admin') {
|
if ($user['role'] !== 'admin') {
|
||||||
// Check if this is the ONLY user, if so, force admin
|
// Check if there are ANY admins in the system
|
||||||
$count = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
|
$adminCount = $pdo->query("SELECT COUNT(*) FROM users WHERE role = 'admin'")->fetchColumn();
|
||||||
if ($count == 1) {
|
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']);
|
$pdo->query("UPDATE users SET role = 'admin' WHERE id = " . $user['id']);
|
||||||
$user['role'] = 'admin';
|
$user['role'] = 'admin';
|
||||||
$_SESSION['role'] = 'admin';
|
$_SESSION['role'] = 'admin';
|
||||||
} else {
|
} 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') {
|
if ($action === 'update_settings') {
|
||||||
foreach ($_POST['settings'] as $key => $value) {
|
foreach ($_POST['settings'] as $key => $value) {
|
||||||
$stmt = $pdo->prepare("UPDATE settings SET setting_value = ?, updated_at = NOW() WHERE setting_key = ?");
|
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = ?");
|
||||||
$stmt->execute([$value, $key]);
|
$stmt->execute([$key, $value, $value]);
|
||||||
}
|
}
|
||||||
header('Location: admin.php?action=settings&success=1');
|
header('Location: admin.php?action=settings&success=1');
|
||||||
exit;
|
exit;
|
||||||
@ -76,6 +111,37 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
header('Location: admin.php?action=users');
|
header('Location: admin.php?action=users');
|
||||||
exit;
|
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);
|
$settings = $pdo->query("SELECT setting_key, setting_value FROM settings")->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
@ -112,6 +178,7 @@ $stats = [
|
|||||||
background: #1e293b;
|
background: #1e293b;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
.main-content { margin-left: var(--sidebar-width); padding: 40px; }
|
.main-content { margin-left: var(--sidebar-width); padding: 40px; }
|
||||||
.nav-link { color: #cbd5e1; padding: 12px 15px; border-radius: 8px; margin-bottom: 5px; }
|
.nav-link { color: #cbd5e1; padding: 12px 15px; border-radius: 8px; margin-bottom: 5px; }
|
||||||
@ -126,16 +193,21 @@ $stats = [
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="mb-4 px-3">
|
<div class="mb-4 px-3 d-flex justify-content-between align-items-center">
|
||||||
<h5 class="fw-bold mb-0">管理后台</h5>
|
<div>
|
||||||
<small class="text-muted">ADMIN PANEL</small>
|
<h5 class="fw-bold mb-0">管理后台</h5>
|
||||||
|
<small class="text-muted">ADMIN PANEL</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav class="nav flex-column">
|
<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 === '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 === '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 === '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 === '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>
|
<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">
|
<hr class="my-3 border-secondary">
|
||||||
<a class="nav-link" href="dashboard.php"><i class="fas fa-arrow-left"></i> 返回前台</a>
|
<a class="nav-link" href="dashboard.php"><i class="fas fa-arrow-left"></i> 返回前台</a>
|
||||||
@ -143,6 +215,20 @@ $stats = [
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</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">
|
<div class="main-content">
|
||||||
<?php if ($action === 'dashboard'): ?>
|
<?php if ($action === 'dashboard'): ?>
|
||||||
<h4 class="fw-bold mb-4">数据概览</h4>
|
<h4 class="fw-bold mb-4">数据概览</h4>
|
||||||
@ -173,7 +259,15 @@ $stats = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php elseif ($action === 'users'): ?>
|
<?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">
|
<div class="card p-3">
|
||||||
<table class="table align-middle">
|
<table class="table align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
@ -196,6 +290,12 @@ $stats = [
|
|||||||
<td><span class="badge bg-<?= $u['role'] === 'admin' ? 'danger' : 'info' ?>"><?= $u['role'] ?></span></td>
|
<td><span class="badge bg-<?= $u['role'] === 'admin' ? 'danger' : 'info' ?>"><?= $u['role'] ?></span></td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-primary" onclick='editUser(<?= json_encode($u) ?>)'>编辑</button>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@ -203,6 +303,43 @@ $stats = [
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</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 fade" id="userModal" tabindex="-1">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<form class="modal-content" method="POST" action="admin.php?action=update_user">
|
<form class="modal-content" method="POST" action="admin.php?action=update_user">
|
||||||
@ -330,14 +467,22 @@ $stats = [
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="card d-flex flex-column" style="height: 600px;">
|
<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="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>
|
||||||
<div class="card-footer bg-white p-3">
|
<div class="card-footer bg-white p-3">
|
||||||
<div class="input-group">
|
<form id="adminChatForm" onsubmit="event.preventDefault(); sendMessage();" class="input-group">
|
||||||
<input type="text" id="msgInput" class="form-control" placeholder="输入回复内容...">
|
<input type="text" id="adminMsgInput" class="form-control" placeholder="输入回复内容..." autocomplete="off">
|
||||||
<button class="btn btn-primary" onclick="sendMessage()">发送</button>
|
<button type="submit" class="btn btn-primary px-4"><i class="fas fa-paper-plane"></i></button>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -345,71 +490,89 @@ $stats = [
|
|||||||
<script>
|
<script>
|
||||||
let currentChatUser = null;
|
let currentChatUser = null;
|
||||||
async function loadChatUsers() {
|
async function loadChatUsers() {
|
||||||
const res = await fetch('ajax_handler.php?action=get_chat_users');
|
try {
|
||||||
const data = await res.json();
|
const res = await fetch('ajax_handler.php?action=get_chat_users');
|
||||||
if (data.code === 0) {
|
const data = await res.json();
|
||||||
const list = document.getElementById('chatUserList');
|
if (data.code === 0) {
|
||||||
list.innerHTML = '';
|
const list = document.getElementById('chatUserList');
|
||||||
data.data.forEach(u => {
|
list.innerHTML = '';
|
||||||
const div = document.createElement('div');
|
data.data.forEach(u => {
|
||||||
div.className = `p-3 border-bottom cursor-pointer \${currentChatUser === u.id ? 'bg-light' : ''}`;
|
const div = document.createElement('div');
|
||||||
div.style.cursor = 'pointer';
|
div.className = `p-3 border-bottom ${currentChatUser === parseInt(u.id) ? 'bg-light border-start border-4 border-primary' : ''}`;
|
||||||
div.innerHTML = `
|
div.style.cursor = 'pointer';
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
div.innerHTML = `
|
||||||
<strong>\${u.username}</strong>
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
<small class="text-muted">\${u.last_time.split(' ')[1]}</small>
|
<strong class="${u.unread_count > 0 ? 'text-primary' : ''}">${u.username}</strong>
|
||||||
</div>
|
<small class="text-muted" style="font-size: 10px;">${u.last_time.split(' ')[1]}</small>
|
||||||
<div class="text-truncate small \${u.unread_count > 0 ? 'fw-bold text-primary' : 'text-muted'}">\${u.last_message}</div>
|
</div>
|
||||||
`;
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
div.onclick = () => selectUser(u.id, u.username);
|
<div class="text-truncate small ${u.unread_count > 0 ? 'fw-bold' : 'text-muted'}" style="max-width: 150px;">${u.last_message}</div>
|
||||||
list.appendChild(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) {
|
function selectUser(id, name) {
|
||||||
currentChatUser = id;
|
currentChatUser = id;
|
||||||
document.getElementById('chatTitle').textContent = '正在与 ' + name + ' 对话';
|
document.getElementById('chatTitle').textContent = '与 ' + name + ' 对话中';
|
||||||
loadMessages();
|
loadMessages();
|
||||||
loadChatUsers();
|
loadChatUsers();
|
||||||
|
document.getElementById('adminMsgInput').focus();
|
||||||
}
|
}
|
||||||
async function loadMessages() {
|
async function loadMessages() {
|
||||||
if (!currentChatUser) return;
|
if (!currentChatUser) return;
|
||||||
const res = await fetch('ajax_handler.php?action=get_messages&user_id=' + currentChatUser);
|
try {
|
||||||
const data = await res.json();
|
const res = await fetch('ajax_handler.php?action=get_messages&user_id=' + currentChatUser);
|
||||||
if (data.code === 0) {
|
const data = await res.json();
|
||||||
const content = document.getElementById('chatContent');
|
if (data.code === 0) {
|
||||||
content.innerHTML = '';
|
const content = document.getElementById('chatContent');
|
||||||
data.data.forEach(m => {
|
content.innerHTML = '';
|
||||||
const isMe = m.sender === 'admin';
|
data.data.forEach(m => {
|
||||||
content.innerHTML += `
|
const isMe = m.sender === 'admin';
|
||||||
<div class="mb-3 d-flex \${isMe ? 'justify-content-end' : ''}">
|
const div = document.createElement('div');
|
||||||
<div class="p-2 px-3 rounded-4 shadow-sm" style="max-width: 80%; background: \${isMe ? '#3b82f6; color: white;' : 'white;'}">
|
div.className = `mb-3 d-flex ${isMe ? 'justify-content-end' : ''}`;
|
||||||
<div>\${m.message}</div>
|
div.innerHTML = `
|
||||||
<small class="opacity-75" style="font-size: 10px;">\${m.created_at}</small>
|
<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>
|
||||||
</div>
|
`;
|
||||||
`;
|
content.appendChild(div);
|
||||||
});
|
});
|
||||||
content.scrollTop = content.scrollHeight;
|
content.scrollTop = content.scrollHeight;
|
||||||
}
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
const input = document.getElementById('msgInput');
|
const input = document.getElementById('adminMsgInput');
|
||||||
const msg = input.value.trim();
|
const msg = input.value.trim();
|
||||||
if (!msg || !currentChatUser) return;
|
if (!msg || !currentChatUser) return;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('message', msg);
|
formData.append('message', msg);
|
||||||
formData.append('user_id', currentChatUser);
|
formData.append('user_id', currentChatUser);
|
||||||
const res = await fetch('ajax_handler.php?action=send_message', { method: 'POST', body: formData });
|
|
||||||
const data = await res.json();
|
input.value = '';
|
||||||
if (data.code === 0) {
|
try {
|
||||||
input.value = '';
|
const res = await fetch('ajax_handler.php?action=send_message', { method: 'POST', body: formData });
|
||||||
loadMessages();
|
const data = await res.json();
|
||||||
loadChatUsers();
|
if (data.code === 0) {
|
||||||
}
|
loadMessages();
|
||||||
|
loadChatUsers();
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
loadChatUsers();
|
loadChatUsers();
|
||||||
setInterval(() => { loadChatUsers(); loadMessages(); }, 5000);
|
setInterval(() => { loadChatUsers(); loadMessages(); }, 3000);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php elseif ($action === 'settings'): ?>
|
<?php elseif ($action === 'settings'): ?>
|
||||||
@ -455,6 +618,39 @@ $stats = [
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</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 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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
196
ajax_handler.php
196
ajax_handler.php
@ -15,7 +15,7 @@ try {
|
|||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$db_apikey = $stmt->fetchColumn();
|
$db_apikey = $stmt->fetchColumn();
|
||||||
|
|
||||||
// Fallback if direct match fails (e.g. weird characters)
|
// Fallback if direct match fails
|
||||||
if (!$db_apikey) {
|
if (!$db_apikey) {
|
||||||
$settings = $pdo->query("SELECT setting_key, setting_value FROM settings")->fetchAll(PDO::FETCH_KEY_PAIR);
|
$settings = $pdo->query("SELECT setting_key, setting_value FROM settings")->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
foreach ($settings as $k => $v) {
|
foreach ($settings as $k => $v) {
|
||||||
@ -25,9 +25,7 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {}
|
||||||
// Log error
|
|
||||||
}
|
|
||||||
|
|
||||||
$api = new LubanSMS($db_apikey);
|
$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) {
|
function check_trc20_payment($address, $target_amount, $order_time) {
|
||||||
if (!$address || $address == 'TEm1B...TRC20_ADDRESS_HERE') return false;
|
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);
|
$url = "https://apilist.tronscan.org/api/token_trc20/transfers?limit=20&start=0&direction=1&address=" . urlencode($address);
|
||||||
|
|
||||||
$ch = curl_init();
|
$ch = curl_init();
|
||||||
curl_setopt($ch, CURLOPT_URL, $url);
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
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);
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
$response = curl_exec($ch);
|
$response = curl_exec($ch);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
if (!$response) return false;
|
if (!$response) return false;
|
||||||
$data = json_decode($response, true);
|
$data = json_decode($response, true);
|
||||||
if (!isset($data['token_transfers'])) return false;
|
if (!isset($data['token_transfers'])) return false;
|
||||||
|
|
||||||
foreach ($data['token_transfers'] as $tx) {
|
foreach ($data['token_transfers'] as $tx) {
|
||||||
if ($tx['symbol'] !== 'USDT') continue;
|
if ($tx['symbol'] !== 'USDT') continue;
|
||||||
|
|
||||||
$amount = (float)$tx['quant'] / pow(10, $tx['tokenInfo']['tokenDecimal']);
|
$amount = (float)$tx['quant'] / pow(10, $tx['tokenInfo']['tokenDecimal']);
|
||||||
$tx_time = (int)($tx['block_ts'] / 1000);
|
$tx_time = (int)($tx['block_ts'] / 1000);
|
||||||
$order_ts = strtotime($order_time);
|
$order_ts = strtotime($order_time);
|
||||||
|
|
||||||
if (abs($amount - $target_amount) < 0.01 && $tx_time > $order_ts) {
|
if (abs($amount - $target_amount) < 0.01 && $tx_time > $order_ts) {
|
||||||
return $tx['transaction_id'];
|
return $tx['transaction_id'];
|
||||||
}
|
}
|
||||||
@ -84,28 +74,14 @@ try {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'get_countries':
|
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();
|
$res = $api->getCountries();
|
||||||
if ($res && (int)($res['code'] ?? -1) === 0) {
|
echo json_encode(['code' => 0, 'data' => $res['msg'] ?? $res['data'] ?? []], JSON_UNESCAPED_UNICODE);
|
||||||
$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);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'get_services':
|
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'] ?? '';
|
$country = $_GET['country'] ?? '';
|
||||||
$service = $_GET['service'] ?? '';
|
$service = $_GET['service'] ?? '';
|
||||||
$res = $api->getServices($country, $service);
|
$res = $api->getServices($country, $service);
|
||||||
|
|
||||||
if ($res && (int)($res['code'] ?? -1) === 0) {
|
if ($res && (int)($res['code'] ?? -1) === 0) {
|
||||||
$data = $res['msg'] ?? $res['data'] ?? [];
|
$data = $res['msg'] ?? $res['data'] ?? [];
|
||||||
if (!is_array($data)) $data = [];
|
if (!is_array($data)) $data = [];
|
||||||
@ -121,24 +97,14 @@ try {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'get_number':
|
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'] ?? '';
|
$service_id = $_GET['service_id'] ?? '';
|
||||||
$country_name = $_GET['country_name'] ?? '未知国家';
|
$country_name = $_GET['country_name'] ?? '未知国家';
|
||||||
$service_name = $_GET['service_name'] ?? '未知项目';
|
$service_name = $_GET['service_name'] ?? '未知项目';
|
||||||
$price = (float)($_GET['price'] ?? 0);
|
$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 = $pdo->prepare("SELECT balance FROM users WHERE id = ?");
|
||||||
$stmt->execute([$_SESSION['user_id']]);
|
$stmt->execute([$_SESSION['user_id']]);
|
||||||
$balance = (float)$stmt->fetchColumn();
|
$balance = (float)$stmt->fetchColumn();
|
||||||
|
|
||||||
if ($balance < $price) {
|
if ($balance < $price) {
|
||||||
echo json_encode(['code' => 400, 'msg' => '余额不足,请先充值']);
|
echo json_encode(['code' => 400, 'msg' => '余额不足,请先充值']);
|
||||||
break;
|
break;
|
||||||
@ -150,7 +116,6 @@ try {
|
|||||||
try {
|
try {
|
||||||
$stmt = $pdo->prepare("UPDATE users SET balance = balance - ? WHERE id = ?");
|
$stmt = $pdo->prepare("UPDATE users SET balance = balance - ? WHERE id = ?");
|
||||||
$stmt->execute([$price, $_SESSION['user_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 = $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]);
|
$stmt->execute([$_SESSION['user_id'], $res['request_id'], $res['number'], $service_name, $country_name, $price]);
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
@ -166,11 +131,6 @@ try {
|
|||||||
|
|
||||||
case 'check_sms':
|
case 'check_sms':
|
||||||
$request_id = $_GET['request_id'] ?? '';
|
$request_id = $_GET['request_id'] ?? '';
|
||||||
if (!$request_id) {
|
|
||||||
echo json_encode(['code' => 400, 'msg' => 'Request ID is required']);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$res = $api->getSms($request_id);
|
$res = $api->getSms($request_id);
|
||||||
if ($res && (int)($res['code'] ?? -1) === 0 && (string)($res['msg'] ?? '') === 'success') {
|
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 = ?");
|
$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);
|
echo json_encode($res ?: ['code' => 500, 'msg' => 'API Error'], JSON_UNESCAPED_UNICODE);
|
||||||
break;
|
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':
|
case 'create_recharge':
|
||||||
$amount = (float)($_POST['amount'] ?? 0);
|
$amount = (float)($_POST['amount'] ?? 0);
|
||||||
if ($amount < 10) {
|
if ($amount < 10) { echo json_encode(['code' => 400, 'msg' => '最低充值金额为 10 USDT']); break; }
|
||||||
echo json_encode(['code' => 400, 'msg' => '最低充值金额为 10 USDT']);
|
$final_amount = floor($amount) + (rand(1, 99) / 100);
|
||||||
break;
|
$stmt = $pdo->prepare("INSERT INTO recharges (user_id, amount, txid, status) VALUES (?, ?, 'Manual/Auto', 'pending')");
|
||||||
}
|
|
||||||
|
|
||||||
$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')");
|
|
||||||
$stmt->execute([$_SESSION['user_id'], $final_amount]);
|
$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;
|
break;
|
||||||
|
|
||||||
case 'check_recharge_status':
|
case 'check_recharge_status':
|
||||||
$recharge_id = $_GET['recharge_id'] ?? '';
|
$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 = $pdo->prepare("SELECT * FROM recharges WHERE id = ? AND user_id = ?");
|
||||||
$stmt->execute([$recharge_id, $_SESSION['user_id']]);
|
$stmt->execute([$recharge_id, $_SESSION['user_id']]);
|
||||||
$recharge = $stmt->fetch();
|
$recharge = $stmt->fetch();
|
||||||
|
if (!$recharge) { echo json_encode(['code' => 404, 'msg' => '未找到充值订单']); break; }
|
||||||
if (!$recharge) {
|
if ($recharge['status'] === 'completed') { echo json_encode(['code' => 0, 'status' => 'completed']); break; }
|
||||||
echo json_encode(['code' => 404, 'msg' => '未找到充值订单']);
|
echo json_encode(['code' => 0, 'status' => 'pending']);
|
||||||
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']);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'send_message':
|
case 'send_message':
|
||||||
$message = trim($_POST['message'] ?? '');
|
$message = trim($_POST['message'] ?? '');
|
||||||
$target_user_id = $_POST['user_id'] ?? $_SESSION['user_id'];
|
$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 = $pdo->prepare("SELECT role FROM users WHERE id = ?");
|
||||||
$stmt->execute([$_SESSION['user_id']]);
|
$stmt->execute([$_SESSION['user_id']]);
|
||||||
$role = $stmt->fetchColumn();
|
$role = $stmt->fetchColumn();
|
||||||
@ -297,70 +170,57 @@ try {
|
|||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO support_messages (user_id, sender, message, is_read) VALUES (?, ?, ?, 0)");
|
$stmt = $pdo->prepare("INSERT INTO support_messages (user_id, sender, message, is_read) VALUES (?, ?, ?, 0)");
|
||||||
$stmt->execute([$target_user_id, $sender, $message]);
|
$stmt->execute([$target_user_id, $sender, $message]);
|
||||||
echo json_encode(['code' => 0, 'msg' => '已发送'], JSON_UNESCAPED_UNICODE);
|
echo json_encode(['code' => 0, 'msg' => '已发送']);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'get_messages':
|
case 'get_messages':
|
||||||
$target_user_id = $_GET['user_id'] ?? $_SESSION['user_id'];
|
$target_user_id = $_GET['user_id'] ?? $_SESSION['user_id'];
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT role FROM users WHERE id = ?");
|
$stmt = $pdo->prepare("SELECT role FROM users WHERE id = ?");
|
||||||
$stmt->execute([$_SESSION['user_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']) {
|
if (!$isAdmin && (int)$target_user_id !== (int)$_SESSION['user_id']) {
|
||||||
echo json_encode(['code' => 403, 'msg' => '无权查看他人消息']);
|
echo json_encode(['code' => 403, 'msg' => 'Forbidden']); break;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as read
|
|
||||||
if ($isAdmin && (int)$target_user_id !== (int)$_SESSION['user_id']) {
|
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'");
|
$pdo->prepare("UPDATE support_messages SET is_read = 1 WHERE user_id = ? AND sender = 'user'")->execute([$target_user_id]);
|
||||||
$stmt->execute([$target_user_id]);
|
|
||||||
} else if (!$isAdmin) {
|
} else if (!$isAdmin) {
|
||||||
$stmt = $pdo->prepare("UPDATE support_messages SET is_read = 1 WHERE user_id = ? AND sender = 'admin'");
|
$pdo->prepare("UPDATE support_messages SET is_read = 1 WHERE user_id = ? AND sender = 'admin'")->execute([$_SESSION['user_id']]);
|
||||||
$stmt->execute([$_SESSION['user_id']]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT * FROM support_messages WHERE user_id = ? ORDER BY created_at ASC");
|
$stmt = $pdo->prepare("SELECT * FROM support_messages WHERE user_id = ? ORDER BY created_at ASC");
|
||||||
$stmt->execute([$target_user_id]);
|
$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;
|
break;
|
||||||
|
|
||||||
case 'get_chat_users':
|
case 'get_chat_users':
|
||||||
$stmt = $pdo->prepare("SELECT role FROM users WHERE id = ?");
|
$stmt = $pdo->prepare("SELECT role FROM users WHERE id = ?");
|
||||||
$stmt->execute([$_SESSION['user_id']]);
|
$stmt->execute([$_SESSION['user_id']]);
|
||||||
if ($stmt->fetchColumn() !== 'admin') {
|
if ($stmt->fetchColumn() !== 'admin') { echo json_encode(['code' => 403, 'msg' => 'Forbidden']); break; }
|
||||||
echo json_encode(['code' => 403, 'msg' => 'Forbidden']);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $pdo->query("
|
$stmt = $pdo->query("
|
||||||
SELECT u.id, u.username, m.message as last_message, m.created_at as last_time,
|
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
|
(SELECT COUNT(*) FROM support_messages WHERE user_id = u.id AND sender = 'user' AND is_read = 0) as unread_count
|
||||||
FROM users u
|
FROM users u
|
||||||
JOIN (
|
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
|
||||||
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
|
JOIN support_messages m ON m.user_id = u.id AND m.created_at = last_msg.max_time
|
||||||
ORDER BY m.created_at DESC
|
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;
|
break;
|
||||||
|
|
||||||
case 'check_new_messages':
|
case 'check_new_messages':
|
||||||
$stmt = $pdo->prepare("SELECT role FROM users WHERE id = ?");
|
$stmt = $pdo->prepare("SELECT role FROM users WHERE id = ?");
|
||||||
$stmt->execute([$_SESSION['user_id']]);
|
$stmt->execute([$_SESSION['user_id']]);
|
||||||
if ($stmt->fetchColumn() !== 'admin') {
|
$role = $stmt->fetchColumn();
|
||||||
echo json_encode(['code' => 403, 'msg' => 'Forbidden']);
|
if ($role === 'admin') {
|
||||||
break;
|
$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']]);
|
||||||
}
|
}
|
||||||
|
echo json_encode(['code' => 0, 'unread_total' => $stmt->fetchColumn()]);
|
||||||
$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]);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -368,5 +228,5 @@ try {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
echo json_encode(['code' => 500, 'msg' => '系统处理异常: ' . $e->getMessage()], JSON_UNESCAPED_UNICODE);
|
echo json_encode(['code' => 500, 'msg' => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
@ -56,6 +56,7 @@ $site_logo = $settings_sidebar['site_logo'] ?? 'assets/pasted-20260210-082628-83
|
|||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.sidebar .nav-link i {
|
.sidebar .nav-link i {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
@ -94,12 +95,28 @@ $site_logo = $settings_sidebar['site_logo'] ?? 'assets/pasted-20260210-082628-83
|
|||||||
.logout-link:hover {
|
.logout-link:hover {
|
||||||
background-color: #fef2f2 !important;
|
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) {
|
@media (max-width: 992px) {
|
||||||
.sidebar { width: 85px; padding: 2rem 0.8rem; }
|
.sidebar { width: 85px; padding: 2rem 0.8rem; }
|
||||||
.sidebar-brand span, .sidebar .nav-link span, .user-profile-mini { display: none; }
|
.sidebar-brand span, .sidebar .nav-link span, .user-profile-mini { display: none; }
|
||||||
.sidebar-brand { margin-bottom: 2rem; padding: 0; justify-content: center; }
|
.sidebar-brand { margin-bottom: 2rem; padding: 0; justify-content: center; }
|
||||||
.sidebar .nav-link { justify-content: center; padding: 15px; margin-bottom: 10px; }
|
.sidebar .nav-link { justify-content: center; padding: 15px; margin-bottom: 10px; }
|
||||||
|
.badge-notif { top: 5px; right: 5px; }
|
||||||
}
|
}
|
||||||
</style>
|
</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' : '' ?>">
|
<a href="support.php" class="nav-link <?= $current_page === 'support.php' ? 'active' : '' ?>">
|
||||||
<i class="fas fa-headset"></i>
|
<i class="fas fa-headset"></i>
|
||||||
<span>联系客服</span>
|
<span>联系客服</span>
|
||||||
|
<span id="userSupportBadge" class="badge-notif" style="display:none;">0</span>
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@ -148,3 +166,26 @@ $site_logo = $settings_sidebar['site_logo'] ?? 'assets/pasted-20260210-082628-83
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
51
rescue.php
51
rescue.php
@ -3,21 +3,42 @@ session_start();
|
|||||||
require_once __DIR__ . '/db/config.php';
|
require_once __DIR__ . '/db/config.php';
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
|
|
||||||
// Force the only user to be admin
|
if (isset($_SESSION['user_id'])) {
|
||||||
$stmt = $pdo->query("SELECT * FROM users");
|
$userId = $_SESSION['user_id'];
|
||||||
$users = $stmt->fetchAll();
|
$pdo->query("UPDATE users SET role = 'admin' WHERE id = " . $userId);
|
||||||
|
|
||||||
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'];
|
|
||||||
$_SESSION['role'] = 'admin';
|
$_SESSION['role'] = 'admin';
|
||||||
echo "<h1>Account Rescued!</h1>";
|
|
||||||
echo "<p>Your account (<strong>" . htmlspecialchars($user['username']) . "</strong>) has been set as Administrator.</p>";
|
$username = $_SESSION['username'] ?? 'User';
|
||||||
echo "<p><a href='admin.php'>Click here to go to Admin Panel</a></p>";
|
|
||||||
|
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 {
|
} else {
|
||||||
echo "<h1>Rescue failed</h1>";
|
// If not logged in, just make the first found user admin and log them in
|
||||||
echo "<p>System has multiple users. Please login with an admin account.</p>";
|
$stmt = $pdo->query("SELECT * FROM users ORDER BY id ASC LIMIT 1");
|
||||||
echo "<p><a href='index.php'>Go back</a></p>";
|
$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>";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
14
support.php
14
support.php
@ -78,8 +78,8 @@ $user = $stmt->fetch();
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message { margin-bottom: 1.5rem; max-width: 70%; display: flex; flex-direction: column; }
|
.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; }
|
.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 { 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-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; }
|
.message-admin { align-items: flex-start; }
|
||||||
@ -140,10 +140,13 @@ $user = $stmt->fetch();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<audio id="notifSound" src="https://assets.mixkit.co/active_storage/sfx/2354/2354-preview.mp3" preload="auto"></audio>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const chatBody = document.getElementById('chatBody');
|
const chatBody = document.getElementById('chatBody');
|
||||||
const chatForm = document.getElementById('chatForm');
|
const chatForm = document.getElementById('chatForm');
|
||||||
const msgInput = document.getElementById('msgInput');
|
const msgInput = document.getElementById('msgInput');
|
||||||
|
const notifSound = document.getElementById('notifSound');
|
||||||
let lastMsgCount = 0;
|
let lastMsgCount = 0;
|
||||||
|
|
||||||
async function loadMessages() {
|
async function loadMessages() {
|
||||||
@ -167,6 +170,13 @@ $user = $stmt->fetch();
|
|||||||
chatBody.appendChild(div);
|
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;
|
lastMsgCount = data.data.length;
|
||||||
chatBody.scrollTop = chatBody.scrollHeight;
|
chatBody.scrollTop = chatBody.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user