-
`;
+ }
+ if (!displayMsg.includes('chat-img-preview')) {
+ displayMsg = displayMsg.replace('
-
-
+
@@ -335,12 +336,20 @@ function appendMessageHTML(m) {
const sender = m.sender;
const text = (m.message || '').toString();
const time = m.created_at || new Date().toISOString();
- const isImage = text.indexOf('
-
';
+ $message = '
';
if (isset($_SESSION['admin_id'])) {
$user_id = (int)($_POST['user_id'] ?? 0);
$ip = $_POST['ip_address'] ?? '';
+ // If IP is missing, try to get it from messages or fallback to current IP
+ if (empty($ip)) {
+ if ($user_id != 0) {
+ $stmt = db()->prepare("SELECT ip_address FROM messages WHERE user_id = ? AND ip_address != '' ORDER BY id DESC LIMIT 1");
+ $stmt->execute([$user_id]);
+ $ip = $stmt->fetchColumn() ?: getRealIP();
+ } else {
+ $ip = getRealIP();
+ }
+ }
$sender = 'admin';
$admin_id = $_SESSION['admin_id'];
$stmt = db()->prepare("INSERT INTO messages (user_id, admin_id, sender, message, ip_address) VALUES (?, ?, ?, ?, ?)");
@@ -40,6 +50,14 @@ if ($action === 'upload_image' || (isset($_POST['action']) && $_POST['action'] =
} else {
$user_id = (int)($_SESSION['user_id'] ?? 0);
$ip = getRealIP();
+
+ // Fallback: If user_id is 0 but we find a user with this registration IP, associate it
+ if ($user_id === 0) {
+ $stmt = db()->prepare("SELECT id FROM users WHERE registration_ip = ? OR last_login_ip = ? ORDER BY id DESC LIMIT 1");
+ $stmt->execute([$ip, $ip]);
+ $user_id = (int)($stmt->fetchColumn() ?: 0);
+ }
+
$sender = 'user';
$stmt = db()->prepare("INSERT INTO messages (user_id, sender, message, ip_address) VALUES (?, ?, ?, ?)");
$stmt->execute([$user_id, $sender, $message, $ip]);
@@ -47,6 +65,14 @@ if ($action === 'upload_image' || (isset($_POST['action']) && $_POST['action'] =
$newId = db()->lastInsertId();
$createdAt = date('Y-m-d H:i:s');
+
+ // Update visitors table to ensure immediate visibility for both users and admins
+ if ($sender === 'user') {
+ $user_time = date('H:i:s');
+ $stmt = db()->prepare("INSERT INTO chat_visitors (user_id, ip_address, user_time) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE last_ping = CURRENT_TIMESTAMP");
+ $stmt->execute([$user_id, $ip, $user_time]);
+ }
+
echo json_encode([
'success' => true,
'url' => $imageUrl,
@@ -70,12 +96,23 @@ if ($action === 'get_messages') {
// If admin is requesting, we use the provided user_id and ip
if (isset($_SESSION['admin_id'])) {
- $stmt = db()->prepare("SELECT * FROM messages WHERE (user_id = ? AND user_id != 0) OR (user_id = 0 AND ip_address = ?) ORDER BY created_at ASC");
+ $stmt = db()->prepare("SELECT * FROM messages WHERE (user_id = ? AND user_id != 0) OR (ip_address = ? AND ip_address != '') ORDER BY created_at ASC");
$stmt->execute([$target_user_id, $target_ip]);
} else {
// User requesting their own messages
- $stmt = db()->prepare("SELECT * FROM messages WHERE (user_id = ? AND user_id != 0) OR (user_id = 0 AND ip_address = ?) ORDER BY created_at ASC");
- $stmt->execute([$user_id, getRealIP()]);
+ // If logged in, get by user_id. Also get by IP to catch visitor history.
+ $user_id = (int)($_SESSION['user_id'] ?? 0);
+ $ip = getRealIP();
+
+ // Fallback: If user_id is 0 but we find a user with this IP, associate it
+ if ($user_id === 0) {
+ $stmt = db()->prepare("SELECT id FROM users WHERE registration_ip = ? OR last_login_ip = ? ORDER BY id DESC LIMIT 1");
+ $stmt->execute([$ip, $ip]);
+ $user_id = (int)($stmt->fetchColumn() ?: 0);
+ }
+
+ $stmt = db()->prepare("SELECT * FROM messages WHERE (user_id = ? AND user_id != 0) OR (ip_address = ? AND ip_address != '') ORDER BY created_at ASC");
+ $stmt->execute([$user_id, $ip]);
}
$messages = $stmt->fetchAll();
@@ -89,6 +126,13 @@ if ($action === 'send_message') {
$user_id = (int)($_SESSION['user_id'] ?? 0);
$ip = getRealIP();
+
+ // Fallback: If user_id is 0 but we find a user with this registration IP, associate it
+ if ($user_id === 0) {
+ $stmt = db()->prepare("SELECT id FROM users WHERE registration_ip = ? OR last_login_ip = ? ORDER BY id DESC LIMIT 1");
+ $stmt->execute([$ip, $ip]);
+ $user_id = (int)($stmt->fetchColumn() ?: 0);
+ }
$stmt = db()->prepare("INSERT INTO messages (user_id, sender, message, ip_address) VALUES (?, ?, ?, ?)");
$stmt->execute([$user_id, 'user', $message, $ip]);
@@ -112,6 +156,17 @@ if ($action === 'admin_send') {
if (!$message) exit(json_encode(['success' => false, 'error' => 'Empty message']));
+ // Robust IP matching: if target_ip is empty but user_id is set, try to find their last IP
+ if (empty($target_ip)) {
+ if ($user_id != 0) {
+ $stmt = db()->prepare("SELECT ip_address FROM messages WHERE user_id = ? AND ip_address != '' ORDER BY id DESC LIMIT 1");
+ $stmt->execute([$user_id]);
+ $target_ip = $stmt->fetchColumn() ?: getRealIP();
+ } else {
+ $target_ip = getRealIP();
+ }
+ }
+
$admin_id = $_SESSION['admin_id'] ?? 1;
$sender = 'admin';
@@ -141,10 +196,10 @@ if ($action === 'admin_get_all') {
exit;
}
try {
- // Robust query to get all active chat sessions
+ // Robust query to get all active chat sessions, merging guest sessions into user sessions if IP matches
$stmt = db()->query("
SELECT
- v.user_id,
+ v.final_user_id as user_id,
v.ip_address,
CASE
WHEN m.message LIKE '
fetchAll());
diff --git a/assets/images/chat/1771656431_699954ef30d7d.png b/assets/images/chat/1771656431_699954ef30d7d.png
new file mode 100644
index 0000000..8a6f72d
Binary files /dev/null and b/assets/images/chat/1771656431_699954ef30d7d.png differ
diff --git a/assets/images/chat/1771656734_6999561e26dde.jpeg b/assets/images/chat/1771656734_6999561e26dde.jpeg
new file mode 100644
index 0000000..a5ac901
Binary files /dev/null and b/assets/images/chat/1771656734_6999561e26dde.jpeg differ
diff --git a/assets/images/chat/1771656835_699956831fd39.png b/assets/images/chat/1771656835_699956831fd39.png
new file mode 100644
index 0000000..8a6f72d
Binary files /dev/null and b/assets/images/chat/1771656835_699956831fd39.png differ
diff --git a/assets/images/chat/1771656847_6999568fb1844.jpeg b/assets/images/chat/1771656847_6999568fb1844.jpeg
new file mode 100644
index 0000000..da83425
Binary files /dev/null and b/assets/images/chat/1771656847_6999568fb1844.jpeg differ
diff --git a/assets/images/chat/1771656995_699957235bc5e.jpeg b/assets/images/chat/1771656995_699957235bc5e.jpeg
new file mode 100644
index 0000000..a5ac901
Binary files /dev/null and b/assets/images/chat/1771656995_699957235bc5e.jpeg differ
diff --git a/assets/images/chat/1771657601_6999598181dec.gif b/assets/images/chat/1771657601_6999598181dec.gif
new file mode 100644
index 0000000..8794888
Binary files /dev/null and b/assets/images/chat/1771657601_6999598181dec.gif differ
diff --git a/assets/images/chat/1771658354_69995c7264127.png b/assets/images/chat/1771658354_69995c7264127.png
new file mode 100644
index 0000000..73b5fb1
Binary files /dev/null and b/assets/images/chat/1771658354_69995c7264127.png differ
diff --git a/assets/images/chat/1771658478_69995ceea47f5.jpeg b/assets/images/chat/1771658478_69995ceea47f5.jpeg
new file mode 100644
index 0000000..a5ac901
Binary files /dev/null and b/assets/images/chat/1771658478_69995ceea47f5.jpeg differ
diff --git a/assets/images/chat/1771658929_69995eb18f7db.jpeg b/assets/images/chat/1771658929_69995eb18f7db.jpeg
new file mode 100644
index 0000000..e107fc0
Binary files /dev/null and b/assets/images/chat/1771658929_69995eb18f7db.jpeg differ
diff --git a/assets/images/chat/1771658953_69995ec9907ab.png b/assets/images/chat/1771658953_69995ec9907ab.png
new file mode 100644
index 0000000..8a6f72d
Binary files /dev/null and b/assets/images/chat/1771658953_69995ec9907ab.png differ
diff --git a/assets/images/chat/1771659438_699960ae5919d.jpeg b/assets/images/chat/1771659438_699960ae5919d.jpeg
new file mode 100644
index 0000000..a5ac901
Binary files /dev/null and b/assets/images/chat/1771659438_699960ae5919d.jpeg differ
diff --git a/assets/images/chat/1771659466_699960ca7fdd7.jpeg b/assets/images/chat/1771659466_699960ca7fdd7.jpeg
new file mode 100644
index 0000000..da83425
Binary files /dev/null and b/assets/images/chat/1771659466_699960ca7fdd7.jpeg differ
diff --git a/assets/images/chat/1771659724_699961cc83bf1.jpg b/assets/images/chat/1771659724_699961cc83bf1.jpg
new file mode 100644
index 0000000..9365b05
Binary files /dev/null and b/assets/images/chat/1771659724_699961cc83bf1.jpg differ
diff --git a/assets/images/chat/1771660505_699964d9a77f0.jpg b/assets/images/chat/1771660505_699964d9a77f0.jpg
new file mode 100644
index 0000000..9365b05
Binary files /dev/null and b/assets/images/chat/1771660505_699964d9a77f0.jpg differ
diff --git a/assets/images/chat/1771660518_699964e630f30.jpeg b/assets/images/chat/1771660518_699964e630f30.jpeg
new file mode 100644
index 0000000..a5ac901
Binary files /dev/null and b/assets/images/chat/1771660518_699964e630f30.jpeg differ
diff --git a/assets/images/chat/1771660790_699965f662dcf.jpeg b/assets/images/chat/1771660790_699965f662dcf.jpeg
new file mode 100644
index 0000000..f51a14a
Binary files /dev/null and b/assets/images/chat/1771660790_699965f662dcf.jpeg differ
diff --git a/assets/images/chat/1771660806_69996606a0afb.png b/assets/images/chat/1771660806_69996606a0afb.png
new file mode 100644
index 0000000..c2e55aa
Binary files /dev/null and b/assets/images/chat/1771660806_69996606a0afb.png differ
diff --git a/auth/register.php b/auth/register.php
index 8d3c533..b7c1b3f 100644
--- a/auth/register.php
+++ b/auth/register.php
@@ -45,7 +45,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $username . '@user.byro'; // Fallback
}
- $ip = $_SERVER['REMOTE_ADDR'] ?? '';
+ $ip = getRealIP();
$stmt = db()->prepare("INSERT INTO users (username, email, password_hash, uid, credit_score, total_recharge, role, registration_ip) VALUES (?, ?, ?, ?, ?, 0, 'user', ?)");
$stmt->execute([$username, $email, $hash, $uid, 80, $ip]);
$userId = db()->lastInsertId();
diff --git a/includes/footer.php b/includes/footer.php
index 312bcca..edb37cf 100644
--- a/includes/footer.php
+++ b/includes/footer.php
@@ -60,6 +60,7 @@
用户名称 (UID: ---)
-
- 实时 IP: ---
- |
- 用户时间: ---
+
撤回` : '';
+
+ let displayMsg = (m.message || '').toString();
+ const isImage = displayMsg.includes('
`;
+ }
+ if (!displayMsg.includes('chat-img-preview')) {
+ displayMsg = displayMsg.replace('
${displayMsg}
-
+
@@ -260,113 +316,124 @@ let currentUserContext = '';
let lastMsgCount = 0;
let notifySound = new Audio('https://assets.mixkit.co/active_storage/sfx/2358/2358-preview.mp3');
-async function refreshUsers() {
- try {
- const list = document.getElementById('user-list');
- if (!list) return;
+ async function refreshUsers() {
+ try {
+ const list = document.getElementById('user-list');
+ if (!list) return;
- const searchInput = document.getElementById('user-search');
- const search = searchInput ? searchInput.value.toLowerCase() : '';
-
- const r = await fetch('/api/chat.php?action=admin_get_all');
- if (!r.ok) return;
-
- const users = await r.json();
- if (users.error || !Array.isArray(users)) return;
-
- // Sound notification for new users or new messages
- let currentTotalMsgs = users.reduce((acc, u) => acc + (u.message ? 1 : 0), 0);
- if (lastMsgCount > 0 && currentTotalMsgs > lastMsgCount) {
- notifySound.play().catch(e => {});
- // Visual feedback
- if (document.hidden) {
- document.title = "【新消息】客服系统";
- }
- }
- lastMsgCount = currentTotalMsgs;
-
- // Reset title when active
- window.onfocus = () => { document.title = "客服系统"; };
-
- if (users.length === 0) {
- list.innerHTML = '
+
@@ -245,6 +300,7 @@ ob_start();
+
-
+
+
+
+
用户名称 (UID: ---)
+
+ 实时定位: ---
+ 本地时间: ---
+
-
- 在线
-
+
+
+
+ 在线通话中
+
+
UID: ---
当前IP: ---
+ 注册IP: ---
最近活跃: ---
暂无活跃会话 (720h内)
';
- return;
- }
-
- let html = '';
- users.forEach(u => {
- const userId = u.user_id || 0;
- const username = (u.username || '匿名访客').toString();
- const uid = (u.uid || '---').toString();
- const ip = (u.ip_address || '---').toString();
- const rawRemark = (u.remark || '').toString();
- const userTime = (u.user_time || '---').toString();
+ const searchInput = document.getElementById('user-search');
+ const search = searchInput ? searchInput.value.toLowerCase() : '';
- // Search filter
- if (search && !username.toLowerCase().includes(search) && !ip.includes(search) && !uid.includes(search)) {
+ const r = await fetch('/api/chat.php?action=admin_get_all');
+ if (!r.ok) return;
+
+ const users = await r.json();
+ if (users.error || !Array.isArray(users)) return;
+
+ // Sound notification for new users or new messages
+ let currentTotalMsgs = users.reduce((acc, u) => acc + (u.message ? 1 : 0), 0);
+ if (lastMsgCount > 0 && currentTotalMsgs > lastMsgCount) {
+ notifySound.play().catch(e => {});
+ // Visual feedback
+ if (document.hidden) {
+ document.title = "【新消息】客服系统";
+ }
+ }
+ lastMsgCount = currentTotalMsgs;
+
+ // Reset title when active
+ window.onfocus = () => { document.title = "客服系统"; };
+
+ if (users.length === 0) {
+ list.innerHTML = '暂无活跃会话
';
return;
}
- const createdAt = u.created_at || '';
- const lastTimeStr = createdAt ? createdAt.replace(/-/g, "/") : new Date().toISOString();
- const lastTime = new Date(lastTimeStr);
-
- let lastMsgText = (u.message || '').toString();
- if (lastMsgText.startsWith('[PAYMENT_INFO]')) {
- lastMsgText = '[收款账号信息]';
+ let html = '';
+ users.forEach(u => {
+ const userId = u.user_id || 0;
+ const username = (u.username || '匿名访客').toString();
+ const uid = (u.uid || '---').toString();
+ const ip = (u.ip_address || '---').toString();
+ const rawRemark = (u.remark || '').toString();
+ const userTime = (u.user_time || '---').toString();
+ const registrationIp = (u.registration_ip || '---').toString();
+
+ // Search filter
+ if (search && !username.toLowerCase().includes(search) && !ip.includes(search) && !uid.includes(search)) {
+ return;
+ }
+
+ const createdAt = u.created_at || '';
+ const lastTimeStr = createdAt ? createdAt.replace(/-/g, "/") : new Date().toISOString();
+ const lastTime = new Date(lastTimeStr);
+
+ let lastMsgText = (u.message || '').toString();
+ if (lastMsgText.startsWith('[PAYMENT_INFO]')) {
+ lastMsgText = '[收款账号信息]';
+ }
+
+ const isActive = (selectedIp === ip && selectedUser == userId);
+
+ // Safe strings for onclick
+ const jsName = username.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
+ const jsRemark = rawRemark.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
+
+ html += `
+
+
+
+ `;
+ });
+ list.innerHTML = html || '
+ ${username}
+ ${isNaN(lastTime.getTime()) ? '---' : lastTime.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit'})}
+
+ ${rawRemark ? `[备注: ${rawRemark}]
` : ''}
+ ${lastMsgText}
+
+ UID: ${uid}
+ ${ip}
+
+ 注册IP: ${registrationIp}
+ 未找到匹配的会话
';
+ } catch (err) {
+ console.error('Refresh users failed:', err);
+ const list = document.getElementById('user-list');
+ if (list) {
+ list.innerHTML = `脚本运行错误: ${err.message}
`;
}
-
- const isActive = (selectedIp === ip && selectedUser == userId);
-
- // Safe strings for onclick
- const jsName = username.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
- const jsRemark = rawRemark.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
-
- html += `
-
-
- `;
- });
- list.innerHTML = html || '
- ${username}
- ${isNaN(lastTime.getTime()) ? '---' : lastTime.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit'})}
-
- ${rawRemark ? `[备注: ${rawRemark}]
` : ''}
- ${lastMsgText}
-
- UID: ${uid}
- ${ip}
-
- 未找到匹配的会话
';
- } catch (err) {
- console.error('Refresh users failed:', err);
- const list = document.getElementById('user-list');
- if (list) {
- list.innerHTML = `脚本运行错误: ${err.message}
`;
}
}
-}
-function openChat(userId, ip, name, uid, remark, userTime) {
- selectedUser = userId;
- selectedIp = ip;
- document.getElementById('header-name').innerText = name;
- document.getElementById('header-uid').innerText = uid;
- document.getElementById('info-ip-header').innerText = ip;
- document.getElementById('info-user-time').innerText = userTime;
- document.getElementById('chat-header').style.display = 'block';
- document.getElementById('input-area').style.display = 'block';
- document.getElementById('remark-area').style.display = 'block';
-
- document.getElementById('remark-text').value = remark;
- document.getElementById('info-uid').innerText = uid;
- document.getElementById('info-ip').innerText = ip;
-
- lastMsgId = 0;
- fetchMessages();
- refreshUsers();
-}
+ function openChat(userId, ip, name, uid, remark, userTime, regIp) {
+ selectedUser = userId;
+ selectedIp = ip;
+ document.getElementById('header-name').innerText = name;
+ document.getElementById('header-uid').innerText = uid;
+ document.getElementById('info-ip-header').innerText = ip;
+ document.getElementById('info-user-time').innerText = userTime;
+ document.getElementById('chat-header').style.display = 'block';
+ document.getElementById('input-area').style.display = 'block';
+ document.getElementById('remark-area').style.display = 'block';
+
+ // IP Location fetch
+ fetch(`https://ipapi.co/${ip}/json/`).then(r => r.json()).then(data => {
+ if (data.city) {
+ document.getElementById('info-ip-header').innerText = `${ip} (${data.city}, ${data.country_name})`;
+ }
+ }).catch(() => {});
+
+ document.getElementById('remark-text').value = remark;
+ document.getElementById('info-uid').innerText = uid;
+ document.getElementById('info-ip').innerText = ip;
+ document.getElementById('info-reg-ip').innerText = regIp || '---';
+
+ lastMsgId = 0;
+ fetchMessages();
+ refreshUsers();
+ }
async function recallMessage(msgId) {
if (!confirm('确定撤回该消息吗?')) return;
@@ -379,6 +446,32 @@ async function recallMessage(msgId) {
}
}
+async function deleteChat(userId, ip, event) {
+ if (event) event.stopPropagation();
+ if (!confirm('确定删除该会话及其所有记录吗?')) return;
+ const fd = new URLSearchParams();
+ fd.append('user_id', userId);
+ fd.append('ip_address', ip);
+ const r = await fetch('/api/chat.php?action=admin_delete_user', { method: 'POST', body: fd });
+ const res = await r.json();
+ if (res.success) {
+ if (selectedUser == userId && selectedIp == ip) {
+ selectedUser = null;
+ selectedIp = null;
+ document.getElementById('chat-header').style.display = 'none';
+ document.getElementById('input-area').style.display = 'none';
+ document.getElementById('remark-area').style.display = 'none';
+ document.getElementById('messages-area').innerHTML = `
+
+
+
+ `;
+ }
+ refreshUsers();
+ }
+}
+
async function deleteUser() {
if (!confirm('确定删除该用户的所有聊天记录吗?此操作不可恢复!')) return;
const fd = new URLSearchParams();
@@ -442,26 +535,41 @@ async function fetchMessages() {
}
}
-function appendMessageHTML(m) {
- const area = document.getElementById('messages-area');
- if (!area) return;
+ function appendMessageHTML(m) {
+ const area = document.getElementById('messages-area');
+ if (!area) return;
- const time = m.created_at || new Date().toISOString();
- const msgDate = time.includes('-') ? new Date(time.replace(/-/g, "/")) : new Date(time);
- const timeStr = msgDate.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
- const recallHtml = m.sender === 'admin' ? `撤回` : '';
- const isImage = typeof m.message === 'string' && m.message.indexOf('请从左侧选择一个会话
+${timeStr} ${recallHtml}
+ ${timeStr} ${recallHtml}
`;
area.appendChild(div);
@@ -687,7 +795,7 @@ document.getElementById('user-search').addEventListener('input', refreshUsers);
async function startPolling() {
await refreshUsers();
await fetchMessages();
- setTimeout(startPolling, 3000);
+ setTimeout(startPolling, 1000);
}
startPolling();
diff --git a/admin/layout.php b/admin/layout.php
index 6bfd7fb..404006c 100644
--- a/admin/layout.php
+++ b/admin/layout.php
@@ -507,14 +507,17 @@ function renderAdminPage($content, $title = '后台管理') {
}
diff --git a/api/chat.php b/api/chat.php
index 445e587..cab5b2e 100644
--- a/api/chat.php
+++ b/api/chat.php
@@ -28,11 +28,21 @@ if ($action === 'upload_image' || (isset($_POST['action']) && $_POST['action'] =
$targetPath = $targetDir . $filename;
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
$imageUrl = '/assets/images/chat/' . $filename;
- $message = '
- ${displayMsg}
-
${timeStr}
+
+
`;
@@ -441,6 +452,20 @@ footer a:hover {
color: var(--primary) !important;
}
#cs-messages::-webkit-scrollbar { display: none; }
+.bubble-user {
+ border-radius: 18px 18px 2px 18px !important;
+ background: linear-gradient(135deg, #00c6ff, #0072ff) !important;
+}
+.bubble-admin {
+ border-radius: 18px 18px 18px 2px !important;
+ background: #1e2329 !important;
+}
+.chat-img-preview {
+ transition: transform 0.2s;
+}
+.chat-img-preview:hover {
+ transform: scale(1.02);
+}
+
+
${displayMsg}
+ ${timeStr}
+