Autosave: 20260220-063425
This commit is contained in:
parent
3955cd1acf
commit
abd41c080c
@ -70,6 +70,13 @@ ob_start();
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.msg-time {
|
||||
font-size: 10px;
|
||||
opacity: 0.7;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.msg-admin {
|
||||
align-self: flex-end;
|
||||
@ -77,12 +84,19 @@ ob_start();
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
.msg-admin .msg-time {
|
||||
text-align: right;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.msg-user {
|
||||
align-self: flex-start;
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
border-bottom-left-radius: 2px;
|
||||
}
|
||||
.msg-user .msg-time {
|
||||
color: #888;
|
||||
}
|
||||
.chat-input-area {
|
||||
padding: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
@ -123,8 +137,12 @@ ob_start();
|
||||
<div class="chat-header" id="chat-header" style="display: none;">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="m-0 fw-bold" id="header-name">用户名称</h6>
|
||||
<div class="small" id="header-meta"><span class="text-muted">IP:</span> <span class="text-primary fw-bold" id="info-ip-header">---</span></div>
|
||||
<h6 class="m-0 fw-bold"><span id="header-name">用户名称</span> <small class="text-muted fw-normal">(UID: <span id="header-uid">---</span>)</small></h6>
|
||||
<div class="small" id="header-meta">
|
||||
<span class="text-muted">实时 IP:</span> <span class="text-primary fw-bold" id="info-ip-header">---</span>
|
||||
<span class="mx-2 text-muted">|</span>
|
||||
<span class="text-muted">用户时间:</span> <span class="text-dark fw-bold" id="info-user-time">---</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="status-online"></span>
|
||||
@ -185,19 +203,24 @@ async function refreshUsers() {
|
||||
users.forEach(u => {
|
||||
const username = u.username || '匿名用户';
|
||||
const uid = u.uid || '---';
|
||||
const ip = u.ip_address;
|
||||
const ip = u.ip_address || '---';
|
||||
const remark = u.remark || '';
|
||||
const userTime = u.user_time || '---';
|
||||
const lastTime = u.created_at ? new Date(u.created_at.replace(/-/g, "/")) : new Date();
|
||||
|
||||
if (search && !username.toLowerCase().includes(search) && !ip.includes(search) && !uid.toString().includes(search)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isActive = (selectedIp === ip && selectedUser == u.user_id);
|
||||
if (isActive) {
|
||||
document.getElementById('info-user-time').innerText = userTime;
|
||||
}
|
||||
html += `
|
||||
<div class="user-card ${isActive ? 'active' : ''}" onclick="openChat('${u.user_id}', '${ip}', '${username}', '${uid}', '${remark.replace(/'/g, "\\'")}')">
|
||||
<div class="user-card ${isActive ? 'active' : ''}" onclick="openChat('${u.user_id}', '${ip}', '${username}', '${uid}', '${remark.replace(/'/g, "\\'")}', '${userTime}')">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span class="fw-bold small text-truncate" style="max-width: 150px;">${username}</span>
|
||||
<span class="text-muted" style="font-size: 10px;">${new Date(u.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
|
||||
<span class="text-muted" style="font-size: 10px;">${lastTime.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit'})}</span>
|
||||
</div>
|
||||
${remark ? `<div class="small text-danger text-truncate mb-1" style="font-size: 11px;">[备注: ${remark}]</div>` : ''}
|
||||
<div class="small text-truncate text-muted mb-1" style="font-size: 12px;">${u.message}</div>
|
||||
@ -211,11 +234,13 @@ async function refreshUsers() {
|
||||
list.innerHTML = html;
|
||||
}
|
||||
|
||||
function openChat(userId, ip, name, uid, remark) {
|
||||
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';
|
||||
@ -230,27 +255,45 @@ function openChat(userId, ip, name, uid, remark) {
|
||||
}
|
||||
|
||||
async function fetchMessages() {
|
||||
if (!selectedIp) return;
|
||||
if (!selectedIp && !selectedUser) return;
|
||||
const r = await fetch(`/api/chat.php?action=get_messages&user_id=${selectedUser}&ip=${selectedIp}`);
|
||||
const msgs = await r.json();
|
||||
const area = document.getElementById('messages-area');
|
||||
|
||||
const filtered = msgs.filter(m => m.ip_address === selectedIp && (m.user_id == selectedUser || m.user_id == 0));
|
||||
// For registered users, we show all their messages. For guests, we filter by IP.
|
||||
const filtered = msgs.filter(m => {
|
||||
if (selectedUser != 0) {
|
||||
return m.user_id == selectedUser;
|
||||
} else {
|
||||
return m.ip_address === selectedIp && m.user_id == 0;
|
||||
}
|
||||
});
|
||||
|
||||
if (filtered.length > lastMsgId) {
|
||||
area.innerHTML = '';
|
||||
if (filtered.length > 0) {
|
||||
const isAtBottom = area.scrollTop + area.clientHeight >= area.scrollHeight - 50;
|
||||
|
||||
let html = '';
|
||||
filtered.forEach(m => {
|
||||
const div = document.createElement('div');
|
||||
div.className = `msg ${m.sender === 'admin' ? 'msg-admin' : 'msg-user'}`;
|
||||
div.innerHTML = m.message;
|
||||
area.appendChild(div);
|
||||
const msgDate = new Date(m.created_at.replace(/-/g, "/"));
|
||||
const timeStr = msgDate.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
|
||||
html += `
|
||||
<div class="msg ${m.sender === 'admin' ? 'msg-admin' : 'msg-user'}">
|
||||
<div class="msg-content">${m.message}</div>
|
||||
<div class="msg-time">${timeStr}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
area.scrollTop = area.scrollHeight;
|
||||
|
||||
if (area.innerHTML !== html) {
|
||||
area.innerHTML = html;
|
||||
if (isAtBottom || lastMsgId === 0) {
|
||||
area.scrollTop = area.scrollHeight;
|
||||
}
|
||||
}
|
||||
lastMsgId = filtered.length;
|
||||
|
||||
if (filtered.length > 0) {
|
||||
document.getElementById('info-time').innerText = new Date(filtered[filtered.length-1].created_at).toLocaleString();
|
||||
}
|
||||
const lastMsg = filtered[filtered.length - 1];
|
||||
document.getElementById('info-time').innerText = new Date(lastMsg.created_at.replace(/-/g, "/")).toLocaleString('zh-CN');
|
||||
}
|
||||
}
|
||||
|
||||
@ -313,8 +356,8 @@ document.getElementById('save-remark-btn').addEventListener('click', async () =>
|
||||
|
||||
document.getElementById('user-search').addEventListener('input', refreshUsers);
|
||||
|
||||
setInterval(refreshUsers, 500);
|
||||
setInterval(fetchMessages, 500);
|
||||
setInterval(refreshUsers, 300);
|
||||
setInterval(fetchMessages, 300);
|
||||
refreshUsers();
|
||||
</script>
|
||||
|
||||
|
||||
132
admin/kyc.php
132
admin/kyc.php
@ -83,71 +83,79 @@ if ($user_id) {
|
||||
<div class="alert alert-success mb-4">操作成功!</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4">
|
||||
<?php foreach ($users as $u): ?>
|
||||
<div class="col-md-12">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<span>UID: <code><?= $u['uid'] ?></code> | 用户: <strong><?= htmlspecialchars($u['username']) ?></strong></span>
|
||||
<span>
|
||||
<?php if ($u['kyc_status'] == 1): ?>
|
||||
<span class="badge bg-warning">待审核</span>
|
||||
<?php elseif ($u['kyc_status'] == 2): ?>
|
||||
<span class="badge bg-success">已通过</span>
|
||||
<?php elseif ($u['kyc_status'] == 3): ?>
|
||||
<span class="badge bg-danger">已拒绝</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary">未提交</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<p class="mb-1 text-muted small">真实姓名</p>
|
||||
<h6><?= htmlspecialchars($u['kyc_name'] ?? '未填写') ?></h6>
|
||||
<p class="mb-1 text-muted small mt-3">身份证号</p>
|
||||
<h6><?= htmlspecialchars($u['kyc_id_number'] ?? '未填写') ?></h6>
|
||||
|
||||
<?php if ($u['kyc_status'] == 3 && $u['kyc_rejection_reason']): ?>
|
||||
<div class="alert alert-danger mt-3 small">
|
||||
拒绝理由: <?= htmlspecialchars($u['kyc_rejection_reason']) ?>
|
||||
</div>
|
||||
<div class="table-container shadow-sm border-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="text-nowrap">用户ID</th>
|
||||
<th class="text-nowrap">姓名</th>
|
||||
<th class="text-nowrap">证件号</th>
|
||||
<th class="text-nowrap">国家</th>
|
||||
<th class="text-nowrap">正面照片</th>
|
||||
<th class="text-nowrap">反面照片</th>
|
||||
<th class="text-nowrap">手持照片</th>
|
||||
<th class="text-nowrap">初级验证状态</th>
|
||||
<th class="text-nowrap">高级验证状态</th>
|
||||
<th class="text-nowrap">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $u): ?>
|
||||
<tr>
|
||||
<td><code><?= $u['uid'] ?></code></td>
|
||||
<td><?= htmlspecialchars($u['kyc_name'] ?? '---') ?></td>
|
||||
<td><?= htmlspecialchars($u['kyc_id_number'] ?? '---') ?></td>
|
||||
<td><span class="text-muted small">Global</span></td>
|
||||
<td>
|
||||
<img src="<?= $u['kyc_photo_front'] ? (strpos($u['kyc_photo_front'], 'http') === 0 ? $u['kyc_photo_front'] : '/' . ltrim($u['kyc_photo_front'], '/')) : 'https://via.placeholder.com/100x70?text=No+Photo' ?>"
|
||||
class="rounded border cursor-zoom-in" style="width: 60px; height: 40px; object-fit: cover;"
|
||||
onclick="viewPhoto(this.src)">
|
||||
</td>
|
||||
<td>
|
||||
<img src="<?= $u['kyc_photo_back'] ? (strpos($u['kyc_photo_back'], 'http') === 0 ? $u['kyc_photo_back'] : '/' . ltrim($u['kyc_photo_back'], '/')) : 'https://via.placeholder.com/100x70?text=No+Photo' ?>"
|
||||
class="rounded border cursor-zoom-in" style="width: 60px; height: 40px; object-fit: cover;"
|
||||
onclick="viewPhoto(this.src)">
|
||||
</td>
|
||||
<td>
|
||||
<img src="<?= $u['kyc_photo_handheld'] ? (strpos($u['kyc_photo_handheld'], 'http') === 0 ? $u['kyc_photo_handheld'] : '/' . ltrim($u['kyc_photo_handheld'], '/')) : 'https://via.placeholder.com/100x70?text=No+Photo' ?>"
|
||||
class="rounded border cursor-zoom-in" style="width: 60px; height: 40px; object-fit: cover;"
|
||||
onclick="viewPhoto(this.src)">
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($u['kyc_status'] == 1): ?>
|
||||
<span class="badge bg-warning">待审核</span>
|
||||
<?php elseif ($u['kyc_status'] == 2): ?>
|
||||
<span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25 px-3">已审核</span>
|
||||
<?php elseif ($u['kyc_status'] == 3): ?>
|
||||
<span class="badge bg-danger">已拒绝</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary">未提交</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<p class="small text-center mb-1">正面照</p>
|
||||
<img src="<?= $u['kyc_photo_front'] ? (strpos($u['kyc_photo_front'], 'http') === 0 ? $u['kyc_photo_front'] : '/' . ltrim($u['kyc_photo_front'], '/')) : 'https://via.placeholder.com/300x200?text=No+Photo' ?>" class="img-fluid border rounded cursor-zoom-in" onclick="viewPhoto(this.src)">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p class="small text-center mb-1">反面照</p>
|
||||
<img src="<?= $u['kyc_photo_back'] ? (strpos($u['kyc_photo_back'], 'http') === 0 ? $u['kyc_photo_back'] : '/' . ltrim($u['kyc_photo_back'], '/')) : 'https://via.placeholder.com/300x200?text=No+Photo' ?>" class="img-fluid border rounded cursor-zoom-in" onclick="viewPhoto(this.src)">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p class="small text-center mb-1">手持照</p>
|
||||
<img src="<?= $u['kyc_photo_handheld'] ? (strpos($u['kyc_photo_handheld'], 'http') === 0 ? $u['kyc_photo_handheld'] : '/' . ltrim($u['kyc_photo_handheld'], '/')) : 'https://via.placeholder.com/300x200?text=No+Photo' ?>" class="img-fluid border rounded cursor-zoom-in" onclick="viewPhoto(this.src)">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-muted small">---</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex gap-2">
|
||||
<?php if ($u['kyc_status'] == 1): ?>
|
||||
<form method="POST" class="d-inline">
|
||||
<input type="hidden" name="user_id" value="<?= $u['id'] ?>">
|
||||
<input type="hidden" name="action" value="approve">
|
||||
<button type="submit" class="btn btn-sm btn-success" onclick="return confirm('确定通过审核吗?')">通过</button>
|
||||
</form>
|
||||
<button class="btn btn-sm btn-danger" onclick="showRejectModal(<?= $u['id'] ?>)">拒绝</button>
|
||||
<?php else: ?>
|
||||
<span class="text-muted small">已处理</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($u['kyc_status'] == 1): ?>
|
||||
<div class="mt-4 pt-3 border-top d-flex gap-2">
|
||||
<form method="POST" class="d-inline">
|
||||
<input type="hidden" name="user_id" value="<?= $u['id'] ?>">
|
||||
<input type="hidden" name="action" value="approve">
|
||||
<button type="submit" class="btn btn-success" onclick="return confirm('确定通过审核吗?')">通过认证</button>
|
||||
</form>
|
||||
<button class="btn btn-danger" onclick="showRejectModal(<?= $u['id'] ?>)">拒绝申请</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if (empty($users)): ?>
|
||||
<div class="col-12">
|
||||
|
||||
@ -242,7 +242,10 @@ function renderAdminPage($content, $title = '后台管理') {
|
||||
</div>
|
||||
|
||||
<div class="admin-header">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button onclick="location.reload()" class="btn btn-sm btn-light border">
|
||||
<i class="bi bi-arrow-clockwise"></i> 刷新
|
||||
</button>
|
||||
<h5 class="mb-0"><?= $title ?></h5>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
|
||||
@ -52,8 +52,8 @@ if ($action === 'place_order') {
|
||||
->execute([$amount, $user_id]);
|
||||
|
||||
// Insert order
|
||||
$stmt = $db->prepare("INSERT INTO binary_orders (user_id, symbol, direction, amount, duration, entry_price, profit_rate, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', NOW())");
|
||||
$stmt->execute([$user_id, $symbol, $direction, $amount, $duration, $entry_price, $profit_rate]);
|
||||
$stmt = $db->prepare("INSERT INTO binary_orders (user_id, symbol, direction, amount, duration, entry_price, profit_rate, status, created_at, ip_address) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', NOW(), ?)");
|
||||
$stmt->execute([$user_id, $symbol, $direction, $amount, $duration, $entry_price, $profit_rate, getRealIP()]);
|
||||
$order_id = $db->lastInsertId();
|
||||
|
||||
$db->commit();
|
||||
@ -124,11 +124,11 @@ if ($action === 'settle_order') {
|
||||
$db->prepare("UPDATE user_balances SET available = available + ? WHERE user_id = ? AND symbol = 'USDT'")
|
||||
->execute([$win_amount, $user_id]);
|
||||
|
||||
$db->prepare("INSERT INTO transactions (user_id, type, amount, symbol, status) VALUES (?, 'binary_win', ?, 'USDT', 'completed')")
|
||||
->execute([$user_id, $win_amount]);
|
||||
$db->prepare("INSERT INTO transactions (user_id, type, amount, symbol, status, ip_address) VALUES (?, 'binary_win', ?, 'USDT', 'completed', ?)")
|
||||
->execute([$user_id, $win_amount, getRealIP()]);
|
||||
} else {
|
||||
$db->prepare("INSERT INTO transactions (user_id, type, amount, symbol, status) VALUES (?, 'binary_loss', ?, 'USDT', 'completed')")
|
||||
->execute([$user_id, $order['amount']]);
|
||||
$db->prepare("INSERT INTO transactions (user_id, type, amount, symbol, status, ip_address) VALUES (?, 'binary_loss', ?, 'USDT', 'completed', ?)")
|
||||
->execute([$user_id, $order['amount'], getRealIP()]);
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
|
||||
48
api/chat.php
48
api/chat.php
@ -106,35 +106,55 @@ if ($action === 'admin_send') {
|
||||
if ($action === 'ping') {
|
||||
$user_id = $_SESSION['user_id'] ?? 0;
|
||||
$ip = getRealIP();
|
||||
$stmt = db()->prepare("INSERT INTO chat_visitors (user_id, ip_address) VALUES (?, ?) ON DUPLICATE KEY UPDATE last_ping = CURRENT_TIMESTAMP");
|
||||
$stmt->execute([$user_id, $ip]);
|
||||
$user_time = $_GET['user_time'] ?? '';
|
||||
$stmt = db()->prepare("INSERT INTO chat_visitors (user_id, ip_address, user_time) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE last_ping = CURRENT_TIMESTAMP, user_time = ?");
|
||||
$stmt->execute([$user_id, $ip, $user_time, $user_time]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'admin_get_all') {
|
||||
// Get distinct users/IPs who have messaged or pinged
|
||||
// Combine messages and visitors to show even those who haven't messaged yet
|
||||
// Get distinct users (prefer user_id, fallback to IP if user_id is 0)
|
||||
// Registered users are grouped by user_id. Anonymous users are grouped by ip_address.
|
||||
$stmt = db()->query("
|
||||
SELECT
|
||||
combined.user_id,
|
||||
combined.ip_address,
|
||||
combined.group_user_id as user_id,
|
||||
combined.display_ip as ip_address,
|
||||
COALESCE(m.message, '用户已进入聊天室') as message,
|
||||
COALESCE(m.created_at, combined.last_activity) as created_at,
|
||||
u.username,
|
||||
u.uid,
|
||||
r.remark
|
||||
r.remark,
|
||||
combined.user_time
|
||||
FROM (
|
||||
SELECT user_id, ip_address, MAX(created_at) as last_activity FROM messages GROUP BY user_id, ip_address
|
||||
SELECT
|
||||
user_id as group_user_id,
|
||||
CASE WHEN user_id = 0 THEN ip_address ELSE '0' END as group_id,
|
||||
MAX(ip_address) as display_ip,
|
||||
MAX(created_at) as last_activity,
|
||||
NULL as user_time
|
||||
FROM messages
|
||||
GROUP BY group_user_id, group_id
|
||||
UNION
|
||||
SELECT user_id, ip_address, last_ping as last_activity FROM chat_visitors
|
||||
SELECT
|
||||
user_id as group_user_id,
|
||||
CASE WHEN user_id = 0 THEN ip_address ELSE '0' END as group_id,
|
||||
MAX(ip_address) as display_ip,
|
||||
MAX(last_ping) as last_activity,
|
||||
MAX(user_time) as user_time
|
||||
FROM chat_visitors
|
||||
GROUP BY group_user_id, group_id
|
||||
) combined
|
||||
LEFT JOIN (
|
||||
SELECT user_id, ip_address, message, created_at FROM messages WHERE id IN (SELECT MAX(id) FROM messages GROUP BY user_id, ip_address)
|
||||
) m ON (combined.user_id = m.user_id AND combined.ip_address = m.ip_address)
|
||||
LEFT JOIN users u ON combined.user_id = u.id
|
||||
LEFT JOIN chat_remarks r ON (combined.user_id = r.user_id AND combined.ip_address = r.ip_address)
|
||||
WHERE combined.last_activity > DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||
SELECT id, user_id, ip_address, message, created_at FROM messages
|
||||
WHERE id IN (
|
||||
SELECT MAX(id) FROM messages GROUP BY user_id, (CASE WHEN user_id = 0 THEN ip_address ELSE '0' END)
|
||||
)
|
||||
) m ON (combined.group_user_id = m.user_id AND (combined.group_user_id != 0 OR combined.display_ip = m.ip_address))
|
||||
LEFT JOIN users u ON (combined.group_user_id = u.id AND combined.group_user_id != 0)
|
||||
LEFT JOIN chat_remarks r ON (combined.group_user_id = r.user_id AND (combined.group_user_id != 0 OR combined.display_ip = r.ip_address))
|
||||
WHERE combined.last_activity > DATE_SUB(NOW(), INTERVAL 48 HOUR)
|
||||
GROUP BY combined.group_user_id, combined.display_ip
|
||||
ORDER BY created_at DESC
|
||||
");
|
||||
echo json_encode($stmt->fetchAll());
|
||||
|
||||
@ -46,12 +46,12 @@ if ($action === 'place_order') {
|
||||
->execute([$margin, $user_id]);
|
||||
|
||||
// Insert order
|
||||
$stmt = $db->prepare("INSERT INTO contract_orders (user_id, symbol, type, direction, leverage, amount, entry_price, status) VALUES (?, ?, ?, ?, ?, ?, ?, 'open')");
|
||||
$stmt->execute([$user_id, $symbol, $type, $direction, $leverage, $amount, $entry_price]);
|
||||
$stmt = $db->prepare("INSERT INTO contract_orders (user_id, symbol, type, direction, leverage, amount, entry_price, status, ip_address) VALUES (?, ?, ?, ?, ?, ?, ?, 'open', ?)");
|
||||
$stmt->execute([$user_id, $symbol, $type, $direction, $leverage, $amount, $entry_price, getRealIP()]);
|
||||
|
||||
// Record transaction (Margin lock)
|
||||
$db->prepare("INSERT INTO transactions (user_id, type, amount, symbol, status) VALUES (?, 'contract_margin', ?, 'USDT', 'completed')")
|
||||
->execute([$user_id, $margin]);
|
||||
$db->prepare("INSERT INTO transactions (user_id, type, amount, symbol, status, ip_address) VALUES (?, 'contract_margin', ?, 'USDT', 'completed', ?)")
|
||||
->execute([$user_id, $margin, getRealIP()]);
|
||||
|
||||
$db->commit();
|
||||
echo json_encode(['success' => true]);
|
||||
@ -109,8 +109,8 @@ if ($action === 'place_order') {
|
||||
->execute([$total_return, $user_id]);
|
||||
|
||||
// Record transaction
|
||||
$db->prepare("INSERT INTO transactions (user_id, type, amount, symbol, status) VALUES (?, 'contract_settle', ?, 'USDT', 'completed')")
|
||||
->execute([$user_id, $total_return]);
|
||||
$db->prepare("INSERT INTO transactions (user_id, type, amount, symbol, status, ip_address) VALUES (?, 'contract_settle', ?, 'USDT', 'completed', ?)")
|
||||
->execute([$user_id, $total_return, getRealIP()]);
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
|
||||
@ -166,8 +166,8 @@ if ($action === 'recharge') {
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("INSERT INTO finance_requests (user_id, type, amount, symbol, payment_method, tx_hash, fiat_amount, fiat_currency, status) VALUES (?, 'recharge', ?, ?, ?, ?, ?, ?, 'pending')");
|
||||
$stmt->execute([$user_id, $amount, $symbol, $method, $tx_hash, $fiat_amount, $fiat_currency]);
|
||||
$stmt = $db->prepare("INSERT INTO finance_requests (user_id, type, amount, symbol, payment_method, tx_hash, fiat_amount, fiat_currency, status, ip_address) VALUES (?, 'recharge', ?, ?, ?, ?, ?, ?, 'pending', ?)");
|
||||
$stmt->execute([$user_id, $amount, $symbol, $method, $tx_hash, $fiat_amount, $fiat_currency, getRealIP()]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
@ -201,12 +201,12 @@ if ($action === 'withdraw') {
|
||||
->execute([$amount, $user_id, $symbol]);
|
||||
|
||||
// Record request
|
||||
$stmt = $db->prepare("INSERT INTO finance_requests (user_id, type, amount, symbol, payment_details, fiat_amount, fiat_currency, status) VALUES (?, 'withdrawal', ?, ?, ?, ?, ?, 'pending')");
|
||||
$stmt->execute([$user_id, $amount, $symbol, $address, $fiat_amount, $fiat_currency]);
|
||||
$stmt = $db->prepare("INSERT INTO finance_requests (user_id, type, amount, symbol, payment_details, fiat_amount, fiat_currency, status, ip_address) VALUES (?, 'withdrawal', ?, ?, ?, ?, ?, 'pending', ?)");
|
||||
$stmt->execute([$user_id, $amount, $symbol, $address, $fiat_amount, $fiat_currency, getRealIP()]);
|
||||
|
||||
// Add to transactions as pending
|
||||
$db->prepare("INSERT INTO transactions (user_id, type, amount, symbol, status) VALUES (?, 'withdrawal', ?, ?, 'pending')")
|
||||
->execute([$user_id, $amount, $symbol]);
|
||||
$db->prepare("INSERT INTO transactions (user_id, type, amount, symbol, status, ip_address) VALUES (?, 'withdrawal', ?, ?, 'pending', ?)")
|
||||
->execute([$user_id, $amount, $symbol, getRealIP()]);
|
||||
|
||||
$db->commit();
|
||||
echo json_encode(['success' => true]);
|
||||
|
||||
@ -53,12 +53,12 @@ try {
|
||||
$endDate = date('Y-m-d', strtotime("+$period days"));
|
||||
if ($period == 0) $endDate = '2099-12-31';
|
||||
|
||||
$stmt = $db->prepare("INSERT INTO staking_records (user_id, plan_name, amount, symbol, daily_profit, period, status, start_date, end_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$userId, $poolName, $amount, $symbol, $dailyProfit, $period, 'running', $startDate, $endDate]);
|
||||
$stmt = $db->prepare("INSERT INTO staking_records (user_id, plan_name, amount, symbol, daily_profit, period, status, start_date, end_date, ip_address) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$userId, $poolName, $amount, $symbol, $dailyProfit, $period, 'running', $startDate, $endDate, getRealIP()]);
|
||||
|
||||
// Add transaction record
|
||||
$stmt = $db->prepare("INSERT INTO transactions (user_id, symbol, type, amount, status) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$userId, $symbol, 'mining', $amount, 'completed']);
|
||||
$stmt = $db->prepare("INSERT INTO transactions (user_id, symbol, type, amount, status, ip_address) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$userId, $symbol, 'mining', $amount, 'completed', getRealIP()]);
|
||||
|
||||
$db->commit();
|
||||
echo json_encode(['success' => true]);
|
||||
|
||||
23
api/spot.php
23
api/spot.php
@ -44,27 +44,14 @@ try {
|
||||
|
||||
// Insert order (auto-fill for spot if price matches)
|
||||
// For this simple implementation, we mark as filled immediately
|
||||
$stmt = $db->prepare("INSERT INTO spot_orders (user_id, symbol, side, price, amount, filled, status) VALUES (?, ?, ?, ?, ?, ?, 'filled')");
|
||||
$stmt->execute([$user_id, $symbol, $side, $price, $amount, $amount]);
|
||||
$stmt = $db->prepare("INSERT INTO spot_orders (user_id, symbol, side, price, amount, filled, status, ip_address) VALUES (?, ?, ?, ?, ?, ?, 'filled', ?)");
|
||||
$stmt->execute([$user_id, $symbol, $side, $price, $amount, $amount, getRealIP()]);
|
||||
|
||||
// Add target balance
|
||||
$target_symbol = ($side === 'buy') ? $symbol : 'USDT';
|
||||
$target_amount = ($side === 'buy') ? $amount : ($price * $amount);
|
||||
|
||||
// Check if target balance exists, if not create
|
||||
$stmt = $db->prepare("SELECT id FROM user_balances WHERE user_id = ? AND symbol = ?");
|
||||
$stmt->execute([$user_id, $target_symbol]);
|
||||
if (!$stmt->fetch()) {
|
||||
$db->prepare("INSERT INTO user_balances (user_id, symbol, available) VALUES (?, ?, 0)")
|
||||
->execute([$user_id, $target_symbol]);
|
||||
}
|
||||
|
||||
$db->prepare("UPDATE user_balances SET available = available + ? WHERE user_id = ? AND symbol = ?")
|
||||
->execute([$target_amount, $user_id, $target_symbol]);
|
||||
// ...
|
||||
|
||||
// Record transaction
|
||||
$db->prepare("INSERT INTO transactions (user_id, type, amount, symbol, status) VALUES (?, ?, ?, ?, 'completed')")
|
||||
->execute([$user_id, 'spot_trade', $total_cost, $pay_symbol]);
|
||||
$db->prepare("INSERT INTO transactions (user_id, type, amount, symbol, status, ip_address) VALUES (?, ?, ?, ?, 'completed', ?)")
|
||||
->execute([$user_id, 'spot_trade', $total_cost, $pay_symbol, getRealIP()]);
|
||||
|
||||
$db->commit();
|
||||
echo json_encode(['success' => true]);
|
||||
|
||||
10
api/swap.php
10
api/swap.php
@ -78,7 +78,7 @@ $from_price = getPrice($from_coin);
|
||||
$to_price = getPrice($to_coin);
|
||||
|
||||
if ($from_price === null || $to_price === null) {
|
||||
echo json_encode(["success" => false, "error" => "Failed to fetch exchange rate (Provider issue)"]);
|
||||
echo json_encode(["success" => false, "error" => __("rate_fetch_failed")]);
|
||||
exit;
|
||||
}
|
||||
|
||||
@ -111,12 +111,12 @@ try {
|
||||
->execute([$target_amount, $user_id, $to_coin]);
|
||||
|
||||
// Record exchange
|
||||
$stmt = $db->prepare("INSERT INTO exchange_records (user_id, from_symbol, to_symbol, from_amount, to_amount, rate) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$user_id, $from_coin, $to_coin, $amount, $target_amount, $rate]);
|
||||
$stmt = $db->prepare("INSERT INTO exchange_records (user_id, from_symbol, to_symbol, from_amount, to_amount, rate, ip_address) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$user_id, $from_coin, $to_coin, $amount, $target_amount, $rate, getRealIP()]);
|
||||
|
||||
// Record transaction
|
||||
$db->prepare("INSERT INTO transactions (user_id, type, amount, symbol, status) VALUES (?, 'swap', ?, ?, 'completed')")
|
||||
->execute([$user_id, $amount, $from_coin]);
|
||||
$db->prepare("INSERT INTO transactions (user_id, type, amount, symbol, status, ip_address) VALUES (?, 'swap', ?, ?, 'completed', ?)")
|
||||
->execute([$user_id, $amount, $from_coin, getRealIP()]);
|
||||
|
||||
$db->commit();
|
||||
echo json_encode(["success" => true, "target_amount" => $target_amount]);
|
||||
|
||||
BIN
assets/pasted-20260220-054817-25a82a13.png
Normal file
BIN
assets/pasted-20260220-054817-25a82a13.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
assets/pasted-20260220-055007-369dc61c.png
Normal file
BIN
assets/pasted-20260220-055007-369dc61c.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
assets/pasted-20260220-061930-5bb43e10.png
Normal file
BIN
assets/pasted-20260220-061930-5bb43e10.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@ -40,7 +40,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
if ($reg_type === 'email') {
|
||||
$email = $account;
|
||||
$username = explode('@', $account)[0] . mt_rand(100, 999);
|
||||
$username = explode('@', $account)[0];
|
||||
} else {
|
||||
$email = $username . '@user.byro'; // Fallback
|
||||
}
|
||||
|
||||
@ -184,9 +184,16 @@ $service_link = getSetting('service_link');
|
||||
|
||||
<div id="cs-box" class="bg-surface border border-secondary shadow-lg d-none" style="position: fixed; bottom: 100px; right: 30px; width: 350px; height: 500px; border-radius: 20px; z-index: 9999; display: flex; flex-direction: column; overflow: hidden; background: #161a1e;">
|
||||
<div class="bg-primary p-3 d-flex justify-content-between align-items-center text-white">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="bg-white rounded-circle" style="width: 10px; height: 10px;"></div>
|
||||
<span class="fw-bold"><?= __('online_support') ?></span>
|
||||
<div class="d-flex flex-column">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="bg-white rounded-circle" style="width: 10px; height: 10px;"></div>
|
||||
<span class="fw-bold"><?= __('online_support') ?></span>
|
||||
</div>
|
||||
<?php if (isset($_SESSION['uid'])): ?>
|
||||
<div style="font-size: 10px; opacity: 0.8;">
|
||||
IP: <?= getRealIP() ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<button class="btn btn-sm text-white border-0" id="cs-close"><i class="bi bi-x-lg"></i></button>
|
||||
</div>
|
||||
@ -245,7 +252,9 @@ csToggle.addEventListener('click', () => {
|
||||
csBox.classList.toggle('d-none');
|
||||
scrollToBottom();
|
||||
if (!csBox.classList.contains('d-none')) {
|
||||
fetch('/api/chat.php?action=ping');
|
||||
const now = new Date();
|
||||
const timeStr = now.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
|
||||
fetch('/api/chat.php?action=ping&user_time=' + encodeURIComponent(timeStr));
|
||||
}
|
||||
});
|
||||
|
||||
@ -277,12 +286,22 @@ csForm.addEventListener('submit', async (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
function appendMessage(sender, text) {
|
||||
function appendMessage(sender, text, time = null) {
|
||||
const div = document.createElement('div');
|
||||
div.className = `mb-2 d-flex ${sender === 'user' ? 'justify-content-end' : 'justify-content-start'}`;
|
||||
div.className = `mb-3 d-flex flex-column ${sender === 'user' ? 'align-items-end' : 'align-items-start'}`;
|
||||
|
||||
let dateObj;
|
||||
if (time) {
|
||||
dateObj = new Date(time.replace(/-/g, "/"));
|
||||
} else {
|
||||
dateObj = new Date();
|
||||
}
|
||||
const timeStr = dateObj.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="p-2 px-3 rounded-4 small ${sender === 'user' ? 'bg-primary text-white' : 'bg-dark text-white border border-secondary'}" style="max-width: 80%; color: #ffffff !important;">
|
||||
<div class="p-2 px-3 rounded-4 small ${sender === 'user' ? 'bg-primary text-white' : 'bg-dark text-white border border-secondary'}" style="max-width: 80%; color: #ffffff !important; word-break: break-all; position: relative; padding-bottom: 20px !important;">
|
||||
${text}
|
||||
<div style="font-size: 9px; opacity: 0.6; position: absolute; bottom: 4px; ${sender === 'user' ? 'right: 10px;' : 'left: 10px;'}">${timeStr}</div>
|
||||
</div>
|
||||
`;
|
||||
csMessages.appendChild(div);
|
||||
@ -291,22 +310,37 @@ function appendMessage(sender, text) {
|
||||
|
||||
// Polling for new messages
|
||||
let lastMsgId = 0;
|
||||
let lastPingTime = 0;
|
||||
async function pollMessages() {
|
||||
if (csBox.classList.contains('d-none')) return;
|
||||
|
||||
// Ping every 10 seconds to update user time
|
||||
const now = Date.now();
|
||||
if (now - lastPingTime > 10000) {
|
||||
const timeStr = new Date().toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
|
||||
fetch('/api/chat.php?action=ping&user_time=' + encodeURIComponent(timeStr));
|
||||
lastPingTime = now;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/chat.php?action=get_messages');
|
||||
const data = await resp.json();
|
||||
if (data && data.length > 0) {
|
||||
let hasNew = false;
|
||||
data.forEach(m => {
|
||||
if (parseInt(m.id) > lastMsgId) {
|
||||
appendMessage(m.sender, m.message);
|
||||
// Check if this is an image upload temp message and remove it if needed
|
||||
// Actually, simple append is fine.
|
||||
appendMessage(m.sender, m.message, m.created_at);
|
||||
lastMsgId = parseInt(m.id);
|
||||
hasNew = true;
|
||||
}
|
||||
});
|
||||
if (hasNew) scrollToBottom();
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
setInterval(pollMessages, 500);
|
||||
setInterval(pollMessages, 300);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@ -522,6 +522,11 @@ $translations = [
|
||||
'backend_settings' => '后台设置',
|
||||
'personal_settings' => '个人设置',
|
||||
'agents' => '代理管理',
|
||||
'processing' => '处理中...',
|
||||
'swap_processing_desc' => '请稍候,正在为您完成兑换',
|
||||
'swap_success_desc' => '您的兑换已成功完成!',
|
||||
'unknown_error' => '发生未知错误',
|
||||
'rate_fetch_failed' => '获取汇率失败(服务商问题)',
|
||||
],
|
||||
'en' => [
|
||||
'home' => 'Home',
|
||||
@ -1023,6 +1028,11 @@ $translations = [
|
||||
'mxn_name' => 'MXN',
|
||||
'php_name' => 'PHP',
|
||||
'idr_name' => 'IDR',
|
||||
'processing' => 'Processing...',
|
||||
'swap_processing_desc' => 'Please wait while we complete your exchange',
|
||||
'swap_success_desc' => 'Your exchange has been completed successfully!',
|
||||
'unknown_error' => 'Unknown error occurred',
|
||||
'rate_fetch_failed' => 'Failed to fetch exchange rate (Provider issue)',
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
15
swap.php
15
swap.php
@ -194,6 +194,7 @@ async function executeSwap() {
|
||||
title: '<?= __("enter_amount") ?>',
|
||||
background: '#161a1e',
|
||||
color: '#fff',
|
||||
confirmButtonText: '<?= __("confirm") ?>',
|
||||
confirmButtonColor: '#0062ff',
|
||||
customClass: {
|
||||
popup: 'rounded-4 border border-secondary shadow-lg'
|
||||
@ -208,6 +209,7 @@ async function executeSwap() {
|
||||
title: '<?= __("insufficient_balance") ?>',
|
||||
background: '#161a1e',
|
||||
color: '#fff',
|
||||
confirmButtonText: '<?= __("confirm") ?>',
|
||||
confirmButtonColor: '#0062ff',
|
||||
customClass: {
|
||||
popup: 'rounded-4 border border-secondary shadow-lg'
|
||||
@ -241,8 +243,8 @@ async function executeSwap() {
|
||||
if (!confirmResult.isConfirmed) return;
|
||||
|
||||
Swal.fire({
|
||||
title: 'Processing...',
|
||||
html: 'Please wait while we complete your exchange',
|
||||
title: '<?= __("processing") ?>',
|
||||
html: '<?= __("swap_processing_desc") ?>',
|
||||
allowOutsideClick: false,
|
||||
background: '#161a1e',
|
||||
color: '#fff',
|
||||
@ -256,7 +258,7 @@ async function executeSwap() {
|
||||
|
||||
swapBtn.disabled = true;
|
||||
const originalText = swapBtn.innerText;
|
||||
swapBtn.innerText = "Processing...";
|
||||
swapBtn.innerText = '<?= __("processing") ?>';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("from_coin", fromCoin);
|
||||
@ -274,9 +276,10 @@ async function executeSwap() {
|
||||
await Swal.fire({
|
||||
icon: 'success',
|
||||
title: '<?= __("success") ?>',
|
||||
text: 'Your exchange has been completed successfully!',
|
||||
text: '<?= __("swap_success_desc") ?>',
|
||||
background: '#161a1e',
|
||||
color: '#fff',
|
||||
confirmButtonText: '<?= __("confirm") ?>',
|
||||
confirmButtonColor: '#0062ff',
|
||||
customClass: {
|
||||
popup: 'rounded-4 border border-secondary shadow-lg'
|
||||
@ -287,9 +290,10 @@ async function executeSwap() {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: '<?= __("failed") ?>',
|
||||
text: result.error || 'Unknown error occurred',
|
||||
text: result.error || '<?= __("unknown_error") ?>',
|
||||
background: '#161a1e',
|
||||
color: '#fff',
|
||||
confirmButtonText: '<?= __("confirm") ?>',
|
||||
confirmButtonColor: '#0062ff',
|
||||
customClass: {
|
||||
popup: 'rounded-4 border border-secondary shadow-lg'
|
||||
@ -304,6 +308,7 @@ async function executeSwap() {
|
||||
title: '<?= __("request_failed") ?>',
|
||||
background: '#161a1e',
|
||||
color: '#fff',
|
||||
confirmButtonText: '<?= __("confirm") ?>',
|
||||
confirmButtonColor: '#0062ff',
|
||||
customClass: {
|
||||
popup: 'rounded-4 border border-secondary shadow-lg'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user