feat: 重构代码架构 + 新增报表/跟单/输赢统计功能

- 拆分 HomeController → TransactionController, ReportWebController, FollowPlanController
- 新增 Service 层: TransactionService, ReportService, FollowPlanService
- pk10.php JS 抽离为 4 个独立文件 (sound/race/bet/poll)
- 前台新增报表查询页面 (/report) + 跟单计划页面 (/follow-plan)
- 后台新增跟单计划管理 + 用户输赢明细统计
- 封盘状态显示倒计时 (x:xx)
- 音效仅在开奖弹窗打开时播放
- 路由按模块分组整理
- autoload 支持 App\Services 命名空间
This commit is contained in:
li
2026-03-27 18:41:06 +08:00
parent 48cfe4be04
commit 4a94fe36cf
87 changed files with 8681 additions and 646 deletions
+444
View File
@@ -0,0 +1,444 @@
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
<i class="fas fa-robot text-primary mr-3"></i>
Bot 管理
</h1>
<?php
$botOptions = [];
foreach (($bots ?? []) as $botItem) {
$botOptions[] = [
'id' => (int)($botItem['id'] ?? 0),
'name' => (string)($botItem['name'] ?? ''),
];
}
$gameOptions = [];
foreach (($games ?? []) as $gameItem) {
$gameOptions[] = [
'id' => (int)($gameItem['id'] ?? 0),
'name' => (string)($gameItem['name'] ?? ''),
'type' => (string)($gameItem['type'] ?? ''),
];
}
$ruleOptions = [];
foreach (($rules ?? []) as $ruleItem) {
$ruleOptions[] = [
'id' => (int)($ruleItem['id'] ?? 0),
'name' => (string)($ruleItem['name'] ?? ''),
'rule_code' => (string)($ruleItem['rule_code'] ?? ''),
];
}
$groupOptions = [];
foreach (($groups ?? []) as $groupItem) {
$countdown = '';
if (!empty($groupItem['countdown_config'])) {
$decodedCountdown = json_decode((string)$groupItem['countdown_config'], true);
if (is_array($decodedCountdown)) {
$countdown = implode(',', array_map('intval', $decodedCountdown));
}
}
$groupOptions[] = [
'id' => (int)($groupItem['id'] ?? 0),
'bot_id' => (int)($groupItem['bot_id'] ?? 0),
'game_id' => (int)($groupItem['game_id'] ?? 0),
'bet_format_rule_id' => (int)($groupItem['bet_format_rule_id'] ?? 0),
'tg_group_id' => (string)($groupItem['tg_group_id'] ?? ''),
'group_name' => (string)($groupItem['group_name'] ?? ''),
'group_type' => (string)($groupItem['group_type'] ?? 'group'),
'countdown_config' => $countdown,
];
}
$userOptions = [];
foreach (($users ?? []) as $userItem) {
$userOptions[] = [
'id' => (int)($userItem['id'] ?? 0),
'username' => (string)($userItem['username'] ?? ''),
'balance' => number_format((float)($userItem['balance'] ?? 0), 2, '.', ''),
];
}
?>
<div id="botAdminAlert" class="hidden mb-6 rounded-lg border px-4 py-3 text-sm"></div>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 gap-4 mb-6">
<div class="bg-white rounded-xl p-5 card-shadow hover-lift"><div class="flex items-center justify-between"><div><p class="text-gray-500 text-sm">Bot 实例数</p><h3 class="text-2xl font-bold mt-1"><?= (int)($stats['bot_count'] ?? 0) ?></h3></div><div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center"><i class="fas fa-robot text-primary"></i></div></div></div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift"><div class="flex items-center justify-between"><div><p class="text-gray-500 text-sm">群配置数</p><h3 class="text-2xl font-bold mt-1"><?= (int)($stats['group_count'] ?? 0) ?></h3></div><div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center"><i class="fas fa-users text-success"></i></div></div></div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift"><div class="flex items-center justify-between"><div><p class="text-gray-500 text-sm">已绑总账号</p><h3 class="text-2xl font-bold mt-1"><?= (int)($stats['wallet_count'] ?? 0) ?></h3></div><div class="w-10 h-10 rounded-full bg-warning/10 flex items-center justify-center"><i class="fas fa-wallet text-warning"></i></div></div></div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift"><div class="flex items-center justify-between"><div><p class="text-gray-500 text-sm">启用群数</p><h3 class="text-2xl font-bold mt-1"><?= (int)($stats['enabled_group_count'] ?? 0) ?></h3></div><div class="w-10 h-10 rounded-full bg-danger/10 flex items-center justify-center"><i class="fas fa-signal text-danger"></i></div></div></div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift"><div class="flex items-center justify-between"><div><p class="text-gray-500 text-sm">成员映射数</p><h3 class="text-2xl font-bold mt-1"><?= (int)($stats['member_count'] ?? 0) ?></h3></div><div class="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center"><i class="fas fa-user-tag text-indigo-600"></i></div></div></div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift"><div class="flex items-center justify-between"><div><p class="text-gray-500 text-sm">托号数量</p><h3 class="text-2xl font-bold mt-1"><?= (int)($stats['shill_count'] ?? 0) ?></h3></div><div class="w-10 h-10 rounded-full bg-rose-100 flex items-center justify-center"><i class="fas fa-user-secret text-rose-600"></i></div></div></div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift"><div class="flex items-center justify-between"><div><p class="text-gray-500 text-sm">推送日志数</p><h3 class="text-2xl font-bold mt-1"><?= (int)($stats['push_count'] ?? 0) ?></h3></div><div class="w-10 h-10 rounded-full bg-cyan-100 flex items-center justify-center"><i class="fas fa-paper-plane text-cyan-600"></i></div></div></div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift"><div class="flex items-center justify-between"><div><p class="text-gray-500 text-sm">推送成功数</p><h3 class="text-2xl font-bold mt-1"><?= (int)($stats['push_success_count'] ?? 0) ?></h3></div><div class="w-10 h-10 rounded-full bg-emerald-100 flex items-center justify-center"><i class="fas fa-circle-check text-emerald-600"></i></div></div></div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift"><div class="flex items-center justify-between"><div><p class="text-gray-500 text-sm">倒计时群数</p><h3 class="text-2xl font-bold mt-1"><?= (int)($stats['countdown_group_count'] ?? 0) ?></h3></div><div class="w-10 h-10 rounded-full bg-amber-100 flex items-center justify-center"><i class="fas fa-hourglass-half text-amber-600"></i></div></div></div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift"><div class="flex items-center justify-between"><div><p class="text-gray-500 text-sm">动画群数</p><h3 class="text-2xl font-bold mt-1"><?= (int)($stats['animation_group_count'] ?? 0) ?></h3></div><div class="w-10 h-10 rounded-full bg-fuchsia-100 flex items-center justify-center"><i class="fas fa-image text-fuchsia-600"></i></div></div></div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift"><div class="flex items-center justify-between"><div><p class="text-gray-500 text-sm">API 请求数</p><h3 class="text-2xl font-bold mt-1"><?= (int)($stats['api_request_count'] ?? 0) ?></h3></div><div class="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center"><i class="fas fa-arrow-right-arrow-left text-slate-600"></i></div></div></div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift"><div class="flex items-center justify-between"><div><p class="text-gray-500 text-sm">签名通过数</p><h3 class="text-2xl font-bold mt-1"><?= (int)($stats['api_signature_ok_count'] ?? 0) ?></h3></div><div class="w-10 h-10 rounded-full bg-lime-100 flex items-center justify-center"><i class="fas fa-shield-halved text-lime-600"></i></div></div></div>
</div>
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6 mb-6">
<div class="xl:col-span-2 bg-white rounded-xl shadow-md p-6">
<div class="flex items-start justify-between gap-4 mb-6">
<div>
<h2 class="text-xl font-semibold text-gray-800">Bot 实例总览</h2>
<p class="text-sm text-gray-500 mt-1">现在已支持新增、编辑、启停 Bot 实例。</p>
</div>
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs bg-primary/10 text-primary"><i class="fas fa-hammer mr-1"></i> Phase 6 进阶</span>
</div>
<div class="overflow-x-auto">
<table class="w-full bg-white rounded-xl overflow-hidden">
<thead class="bg-gray-50"><tr><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">实例</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">运行模式</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">群数</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">更新时间</th><th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th></tr></thead>
<tbody class="divide-y divide-gray-200">
<?php if (!empty($bots)): foreach ($bots as $bot): ?>
<?php $enabled = (int)($bot['status'] ?? 0) === 1; ?>
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-4 py-4 align-top"><div class="space-y-1"><div class="text-sm font-semibold text-gray-900"><?= htmlspecialchars((string)($bot['name'] ?? '未命名 Bot')) ?></div><div class="text-xs text-gray-500">Key<?= htmlspecialchars((string)($bot['bot_key'] ?? '-')) ?></div><div class="text-xs text-gray-400">@<?= htmlspecialchars((string)($bot['bot_username'] ?? 'unknown')) ?></div></div></td>
<td class="px-4 py-4 align-top text-sm text-gray-600"><?= htmlspecialchars((string)($bot['run_mode'] ?? 'polling')) ?></td>
<td class="px-4 py-4 align-top"><div class="text-sm text-gray-700">总群数:<?= (int)($bot['group_count'] ?? 0) ?></div><div class="text-xs text-gray-500 mt-1">启用下注:<?= (int)($bot['active_group_count'] ?? 0) ?></div></td>
<td class="px-4 py-4 align-top"><span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $enabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-700' ?>"><span class="w-2 h-2 rounded-full mr-1 <?= $enabled ? 'bg-green-500' : 'bg-gray-400' ?>"></span><?= $enabled ? '启用' : '停用' ?></span></td>
<td class="px-4 py-4 align-top text-sm text-gray-500"><?= htmlspecialchars((string)($bot['updated_at'] ?? ($bot['created_at'] ?? '-'))) ?></td>
<td class="px-4 py-4 align-top text-right"><div class="flex items-center justify-end gap-2"><button type="button" class="edit-bot-btn inline-flex items-center px-3 py-1.5 rounded-lg border border-primary/20 text-primary text-xs hover:bg-primary/5" data-id="<?= (int)$bot['id'] ?>" data-name="<?= htmlspecialchars((string)($bot['name'] ?? ''), ENT_QUOTES) ?>" data-token="<?= htmlspecialchars((string)($bot['bot_token'] ?? ''), ENT_QUOTES) ?>" data-username="<?= htmlspecialchars((string)($bot['bot_username'] ?? ''), ENT_QUOTES) ?>" data-run-mode="<?= htmlspecialchars((string)($bot['run_mode'] ?? 'polling'), ENT_QUOTES) ?>" data-webhook-url="<?= htmlspecialchars((string)($bot['webhook_url'] ?? ''), ENT_QUOTES) ?>" data-remark="<?= htmlspecialchars((string)($bot['remark'] ?? ''), ENT_QUOTES) ?>"><i class="fas fa-pen mr-1"></i>编辑</button><button type="button" class="toggle-btn inline-flex items-center px-3 py-1.5 rounded-lg text-xs <?= $enabled ? 'bg-red-50 text-red-600 border border-red-200' : 'bg-green-50 text-green-600 border border-green-200' ?>" data-url="/admin/bots/toggle" data-id="<?= (int)$bot['id'] ?>" data-status="<?= $enabled ? 0 : 1 ?>"><?= $enabled ? '停用' : '启用' ?></button></div></td>
</tr>
<?php endforeach; else: ?><tr><td colspan="6" class="px-4 py-10 text-center text-sm text-gray-500">尚未配置任何 Bot 实例。</td></tr><?php endif; ?>
</tbody>
</table>
</div>
</div>
<div class="bg-white rounded-xl shadow-md p-6 space-y-6">
<div>
<div class="flex items-center justify-between mb-4"><h2 class="text-xl font-semibold text-gray-800">新增 Bot 实例</h2><button type="button" id="cancelBotEditBtn" class="hidden text-xs text-gray-500 hover:text-gray-700">取消编辑</button></div>
<form id="botForm" class="space-y-3">
<input type="hidden" name="id">
<input type="text" name="name" placeholder="Bot 名称" required class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
<input type="text" name="bot_token" placeholder="Telegram Bot Token" required class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
<input type="text" name="bot_username" placeholder="Bot Username,可选" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
<select name="run_mode" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white"><option value="polling">polling</option><option value="webhook">webhook</option></select>
<input type="text" name="webhook_url" placeholder="Webhook URL,可选" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
<textarea name="remark" rows="2" placeholder="备注,可选" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"></textarea>
<button type="submit" class="w-full bg-primary hover:bg-primary/90 text-white px-4 py-2.5 rounded-lg text-sm" data-create-text="创建 Bot 实例" data-update-text="保存 Bot 修改">创建 Bot 实例</button>
</form>
</div>
<div class="border-t pt-6">
<div class="flex items-center justify-between mb-4"><h2 class="text-xl font-semibold text-gray-800">绑定群配置</h2><button type="button" id="cancelGroupEditBtn" class="hidden text-xs text-gray-500 hover:text-gray-700">取消编辑</button></div>
<form id="groupForm" class="space-y-3">
<input type="hidden" name="id">
<select name="bot_id" required class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white"><option value="">选择 Bot</option><?php foreach (($bots ?? []) as $bot): ?><option value="<?= (int)$bot['id'] ?>"><?= htmlspecialchars((string)$bot['name']) ?></option><?php endforeach; ?></select>
<select name="game_id" required class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white"><option value="">选择游戏</option><?php foreach (($games ?? []) as $game): ?><option value="<?= (int)$game['id'] ?>"><?= htmlspecialchars((string)$game['name']) ?> (<?= htmlspecialchars((string)$game['type']) ?>)</option><?php endforeach; ?></select>
<input type="text" name="tg_group_id" placeholder="Telegram 群 ID,例如 -100123456" required class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
<input type="text" name="group_name" placeholder="群名称" required class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
<select name="group_type" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white"><option value="group">group</option><option value="supergroup">supergroup</option><option value="channel">channel</option></select>
<select name="bet_format_rule_id" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white"><option value="">下注规则(可选)</option><?php foreach (($rules ?? []) as $rule): ?><option value="<?= (int)$rule['id'] ?>"><?= htmlspecialchars((string)$rule['name']) ?> / <?= htmlspecialchars((string)$rule['rule_code']) ?></option><?php endforeach; ?></select>
<input type="text" name="countdown_config" placeholder="倒计时配置,如 60,30,10" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
<div class="grid grid-cols-2 gap-3 text-sm text-gray-700"><label class="flex items-center gap-2"><input type="checkbox" name="remind_bet_success" checked>下注成功提醒</label><label class="flex items-center gap-2"><input type="checkbox" name="remind_draw_result" checked>开奖提醒</label><label class="flex items-center gap-2"><input type="checkbox" name="remind_close_countdown" checked>封盘倒计时</label><label class="flex items-center gap-2"><input type="checkbox" name="animation_enabled">开奖动画</label><label class="flex items-center gap-2"><input type="checkbox" name="bet_enabled" checked>允许下注同步</label></div>
<button type="submit" class="w-full bg-success hover:bg-success/90 text-white px-4 py-2.5 rounded-lg text-sm" data-create-text="创建群配置" data-update-text="保存群配置修改">创建群配置</button>
</form>
</div>
<div class="border-t pt-6">
<div class="flex items-center justify-between mb-4"><h2 class="text-xl font-semibold text-gray-800">绑定群总账号</h2><button type="button" id="cancelWalletEditBtn" class="hidden text-xs text-gray-500 hover:text-gray-700">取消编辑</button></div>
<form id="walletForm" class="space-y-3">
<input type="hidden" name="id">
<select name="group_id" required class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white"><option value="">选择群配置</option><?php foreach (($groups ?? []) as $group): ?><option value="<?= (int)$group['id'] ?>"><?= htmlspecialchars((string)$group['group_name']) ?> / <?= htmlspecialchars((string)$group['tg_group_id']) ?></option><?php endforeach; ?></select>
<select name="platform_user_id" required class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white"><option value="">选择网站账号</option><?php foreach (($users ?? []) as $user): ?><option value="<?= (int)$user['id'] ?>"><?= htmlspecialchars((string)$user['username']) ?> / 余额 <?= number_format((float)($user['balance'] ?? 0), 2) ?></option><?php endforeach; ?></select>
<select name="wallet_mode" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white"><option value="master_pool">master_pool</option><option value="per_member">per_member</option></select>
<button type="submit" class="w-full bg-warning hover:bg-warning/90 text-white px-4 py-2.5 rounded-lg text-sm" data-create-text="绑定群总账号" data-update-text="保存总账号修改">绑定群总账号</button>
</form>
</div>
</div>
</div>
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-6">
<div class="bg-white rounded-xl shadow-md p-6">
<h2 class="text-xl font-semibold text-gray-800 mb-4">群配置总览</h2>
<div class="overflow-x-auto"><table class="w-full text-sm"><thead class="bg-gray-50"><tr><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">群</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Bot / 游戏</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">开关</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th><th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th></tr></thead><tbody class="divide-y divide-gray-200"><?php if (!empty($groups)): foreach ($groups as $group): ?><?php $active = (int)($group['status'] ?? 0) === 1; $countdownText = ''; if (!empty($group['countdown_config'])) { $decoded = json_decode((string)$group['countdown_config'], true); if (is_array($decoded) && !empty($decoded)) { $countdownText = implode(',', array_map('intval', $decoded)); } } ?><tr class="hover:bg-gray-50"><td class="px-4 py-4 align-top"><div class="font-medium text-gray-800"><?= htmlspecialchars((string)$group['group_name']) ?></div><div class="text-xs text-gray-500 mt-1"><?= htmlspecialchars((string)$group['tg_group_id']) ?> / <?= htmlspecialchars((string)$group['group_type']) ?></div><?php if ($countdownText !== ''): ?><div class="text-xs text-gray-400 mt-1">倒计时:<?= htmlspecialchars($countdownText) ?></div><?php endif; ?></td><td class="px-4 py-4 align-top text-sm text-gray-600"><div><?= htmlspecialchars((string)($group['bot_name'] ?? '-')) ?></div><div class="text-xs text-gray-500 mt-1"><?= htmlspecialchars((string)($group['game_name'] ?? '-')) ?></div></td><td class="px-4 py-4 align-top text-xs text-gray-600"><div>下注:<?= !empty($group['bet_enabled']) ? '开' : '关' ?></div><div>提醒:<?= !empty($group['remind_bet_success']) || !empty($group['remind_draw_result']) || !empty($group['remind_close_countdown']) ? '开' : '关' ?></div><div>动画:<?= !empty($group['animation_enabled']) ? '开' : '关' ?></div></td><td class="px-4 py-4 align-top"><span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-700' ?>"><?= $active ? '启用' : '停用' ?></span></td><td class="px-4 py-4 align-top text-right"><div class="flex items-center justify-end gap-2"><button type="button" class="edit-group-btn inline-flex items-center px-3 py-1.5 rounded-lg border border-primary/20 text-primary text-xs hover:bg-primary/5" data-id="<?= (int)$group['id'] ?>" data-bot-id="<?= (int)($group['bot_id'] ?? 0) ?>" data-game-id="<?= (int)($group['game_id'] ?? 0) ?>" data-rule-id="<?= (int)($group['bet_format_rule_id'] ?? 0) ?>" data-tg-group-id="<?= htmlspecialchars((string)($group['tg_group_id'] ?? ''), ENT_QUOTES) ?>" data-group-name="<?= htmlspecialchars((string)($group['group_name'] ?? ''), ENT_QUOTES) ?>" data-group-type="<?= htmlspecialchars((string)($group['group_type'] ?? 'group'), ENT_QUOTES) ?>" data-countdown-config="<?= htmlspecialchars($countdownText, ENT_QUOTES) ?>" data-remind-bet-success="<?= !empty($group['remind_bet_success']) ? 1 : 0 ?>" data-remind-draw-result="<?= !empty($group['remind_draw_result']) ? 1 : 0 ?>" data-remind-close-countdown="<?= !empty($group['remind_close_countdown']) ? 1 : 0 ?>" data-animation-enabled="<?= !empty($group['animation_enabled']) ? 1 : 0 ?>" data-bet-enabled="<?= !empty($group['bet_enabled']) ? 1 : 0 ?>"><i class="fas fa-pen mr-1"></i>编辑</button><button type="button" class="toggle-btn inline-flex items-center px-3 py-1.5 rounded-lg text-xs <?= $active ? 'bg-red-50 text-red-600 border border-red-200' : 'bg-green-50 text-green-600 border border-green-200' ?>" data-url="/admin/bots/groups/toggle" data-id="<?= (int)$group['id'] ?>" data-status="<?= $active ? 0 : 1 ?>"><?= $active ? '停用' : '启用' ?></button></div></td></tr><?php endforeach; else: ?><tr><td colspan="5" class="px-4 py-10 text-center text-sm text-gray-500">暂无群配置。</td></tr><?php endif; ?></tbody></table></div>
</div>
<div class="bg-white rounded-xl shadow-md p-6">
<h2 class="text-xl font-semibold text-gray-800 mb-4">群总账号绑定</h2>
<div class="overflow-x-auto"><table class="w-full text-sm"><thead class="bg-gray-50"><tr><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">群</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">网站账号</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">模式</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th><th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th></tr></thead><tbody class="divide-y divide-gray-200"><?php if (!empty($wallets)): foreach ($wallets as $wallet): ?><?php $active = (int)($wallet['status'] ?? 0) === 1; ?><tr class="hover:bg-gray-50"><td class="px-4 py-4 align-top"><div class="font-medium text-gray-800"><?= htmlspecialchars((string)($wallet['group_name'] ?? '-')) ?></div><div class="text-xs text-gray-500 mt-1"><?= htmlspecialchars((string)($wallet['tg_group_id'] ?? '-')) ?></div></td><td class="px-4 py-4 align-top text-sm text-gray-600"><div><?= htmlspecialchars((string)($wallet['platform_username'] ?? '-')) ?></div><div class="text-xs text-gray-500 mt-1">UID <?= (int)($wallet['platform_user_id'] ?? 0) ?> / 余额 <?= number_format((float)($wallet['platform_balance'] ?? 0), 2) ?></div></td><td class="px-4 py-4 align-top text-sm text-gray-600"><?= htmlspecialchars((string)($wallet['wallet_mode'] ?? 'master_pool')) ?></td><td class="px-4 py-4 align-top"><span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-700' ?>"><?= $active ? '启用' : '停用' ?></span></td><td class="px-4 py-4 align-top text-right"><div class="flex items-center justify-end gap-2"><button type="button" class="edit-wallet-btn inline-flex items-center px-3 py-1.5 rounded-lg border border-primary/20 text-primary text-xs hover:bg-primary/5" data-id="<?= (int)$wallet['id'] ?>" data-group-id="<?= (int)($wallet['group_id'] ?? 0) ?>" data-platform-user-id="<?= (int)($wallet['platform_user_id'] ?? 0) ?>" data-wallet-mode="<?= htmlspecialchars((string)($wallet['wallet_mode'] ?? 'master_pool'), ENT_QUOTES) ?>"><i class="fas fa-pen mr-1"></i>编辑</button><button type="button" class="toggle-btn inline-flex items-center px-3 py-1.5 rounded-lg text-xs <?= $active ? 'bg-red-50 text-red-600 border border-red-200' : 'bg-green-50 text-green-600 border border-green-200' ?>" data-url="/admin/bots/wallets/toggle" data-id="<?= (int)$wallet['id'] ?>" data-status="<?= $active ? 0 : 1 ?>"><?= $active ? '停用' : '启用' ?></button></div></td></tr><?php endforeach; else: ?><tr><td colspan="5" class="px-4 py-10 text-center text-sm text-gray-500">暂无群总账号绑定。</td></tr><?php endif; ?></tbody></table></div>
</div>
</div>
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-6">
<div class="bg-white rounded-xl shadow-md p-6 space-y-6">
<div>
<div class="flex items-center justify-between mb-4"><h2 class="text-xl font-semibold text-gray-800">群成员映射</h2><button type="button" id="cancelMemberEditBtn" class="hidden text-xs text-gray-500 hover:text-gray-700">取消编辑</button></div>
<form id="memberForm" class="space-y-3">
<input type="hidden" name="id">
<select name="group_id" required class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white"><option value="">选择群配置</option><?php foreach (($groups ?? []) as $group): ?><option value="<?= (int)$group['id'] ?>"><?= htmlspecialchars((string)$group['group_name']) ?> / <?= htmlspecialchars((string)$group['tg_group_id']) ?></option><?php endforeach; ?></select>
<input type="text" name="tg_user_id" placeholder="TG 用户 ID" required class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
<input type="text" name="tg_username" placeholder="TG Username,可选" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
<input type="text" name="tg_nickname" placeholder="TG 昵称,可选" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
<select name="platform_user_id" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white"><option value="">绑定网站账号(可选)</option><?php foreach (($users ?? []) as $user): ?><option value="<?= (int)$user['id'] ?>"><?= htmlspecialchars((string)$user['username']) ?> / 余额 <?= number_format((float)($user['balance'] ?? 0), 2) ?></option><?php endforeach; ?></select>
<select name="role" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white"><option value="member">member</option><option value="admin">admin</option><option value="shill">shill</option></select>
<input type="text" name="shill_note" placeholder="若标记为托,可填写备注" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
<label class="flex items-center gap-2 text-sm text-gray-700"><input type="checkbox" name="bet_enabled" checked>允许该成员下注同步</label>
<button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2.5 rounded-lg text-sm" data-create-text="创建成员映射" data-update-text="保存成员修改">创建成员映射</button>
</form>
</div>
<div class="border-t pt-6">
<h2 class="text-xl font-semibold text-gray-800 mb-4">成员映射列表</h2>
<div class="overflow-x-auto"><table class="w-full text-sm"><thead class="bg-gray-50"><tr><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">成员</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">群 / 网站账号</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">角色</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">下注</th><th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th></tr></thead><tbody class="divide-y divide-gray-200"><?php if (!empty($members)): foreach ($members as $member): ?><?php $betEnabled = (int)($member['bet_enabled'] ?? 0) === 1; ?><tr class="hover:bg-gray-50"><td class="px-4 py-4 align-top"><div class="font-medium text-gray-800"><?= htmlspecialchars((string)($member['tg_nickname'] ?: $member['tg_username'] ?: $member['tg_user_id'])) ?></div><div class="text-xs text-gray-500 mt-1">UID <?= htmlspecialchars((string)$member['tg_user_id']) ?> / @<?= htmlspecialchars((string)($member['tg_username'] ?? '-')) ?></div></td><td class="px-4 py-4 align-top text-sm text-gray-600"><div><?= htmlspecialchars((string)($member['group_name'] ?? '-')) ?></div><div class="text-xs text-gray-500 mt-1">网站:<?= htmlspecialchars((string)($member['platform_username'] ?? '未绑定')) ?></div></td><td class="px-4 py-4 align-top"><span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= ($member['role'] ?? 'member') === 'shill' ? 'bg-rose-100 text-rose-700' : 'bg-slate-100 text-slate-700' ?>"><?= htmlspecialchars((string)($member['role'] ?? 'member')) ?></span></td><td class="px-4 py-4 align-top"><span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $betEnabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-700' ?>"><?= $betEnabled ? '允许' : '关闭' ?></span></td><td class="px-4 py-4 align-top text-right"><div class="flex items-center justify-end gap-2"><button type="button" class="edit-member-btn inline-flex items-center px-3 py-1.5 rounded-lg border border-primary/20 text-primary text-xs hover:bg-primary/5" data-id="<?= (int)$member['id'] ?>" data-group-id="<?= (int)($member['group_id'] ?? 0) ?>" data-tg-user-id="<?= htmlspecialchars((string)($member['tg_user_id'] ?? ''), ENT_QUOTES) ?>" data-tg-username="<?= htmlspecialchars((string)($member['tg_username'] ?? ''), ENT_QUOTES) ?>" data-tg-nickname="<?= htmlspecialchars((string)($member['tg_nickname'] ?? ''), ENT_QUOTES) ?>" data-platform-user-id="<?= (int)($member['platform_user_id'] ?? 0) ?>" data-role="<?= htmlspecialchars((string)($member['role'] ?? 'member'), ENT_QUOTES) ?>" data-bet-enabled="<?= $betEnabled ? 1 : 0 ?>"><i class="fas fa-pen mr-1"></i>编辑</button><button type="button" class="member-bet-toggle-btn inline-flex items-center px-3 py-1.5 rounded-lg text-xs <?= $betEnabled ? 'bg-red-50 text-red-600 border border-red-200' : 'bg-green-50 text-green-600 border border-green-200' ?>" data-id="<?= (int)$member['id'] ?>" data-bet-enabled="<?= $betEnabled ? 0 : 1 ?>"><?= $betEnabled ? '停用下注' : '启用下注' ?></button></div></td></tr><?php endforeach; else: ?><tr><td colspan="5" class="px-4 py-10 text-center text-sm text-gray-500">暂无成员映射。</td></tr><?php endif; ?></tbody></table></div>
</div>
</div>
<div class="bg-white rounded-xl shadow-md p-6 space-y-6">
<div>
<div class="flex items-center justify-between mb-4"><h2 class="text-xl font-semibold text-gray-800">托号管理</h2><button type="button" id="cancelShillEditBtn" class="hidden text-xs text-gray-500 hover:text-gray-700">取消编辑</button></div>
<form id="shillForm" class="space-y-3">
<input type="hidden" name="id">
<select name="group_id" required class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm bg-white"><option value="">选择群配置</option><?php foreach (($groups ?? []) as $group): ?><option value="<?= (int)$group['id'] ?>"><?= htmlspecialchars((string)$group['group_name']) ?> / <?= htmlspecialchars((string)$group['tg_group_id']) ?></option><?php endforeach; ?></select>
<input type="text" name="tg_user_id" placeholder="TG 用户 ID" required class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
<input type="text" name="note" placeholder="备注,可选" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm">
<button type="submit" class="w-full bg-rose-600 hover:bg-rose-500 text-white px-4 py-2.5 rounded-lg text-sm" data-create-text="创建托号" data-update-text="保存托号修改">创建托号</button>
</form>
</div>
<div class="border-t pt-6">
<h2 class="text-xl font-semibold text-gray-800 mb-4">托号列表</h2>
<div class="overflow-x-auto"><table class="w-full text-sm"><thead class="bg-gray-50"><tr><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">托号</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">群</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">备注</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th><th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th></tr></thead><tbody class="divide-y divide-gray-200"><?php if (!empty($shills)): foreach ($shills as $shill): ?><?php $enabled = (int)($shill['enabled'] ?? 0) === 1; ?><tr class="hover:bg-gray-50"><td class="px-4 py-4 align-top"><div class="font-medium text-gray-800"><?= htmlspecialchars((string)($shill['tg_nickname'] ?: $shill['tg_username'] ?: $shill['tg_user_id'])) ?></div><div class="text-xs text-gray-500 mt-1">UID <?= htmlspecialchars((string)$shill['tg_user_id']) ?></div></td><td class="px-4 py-4 align-top text-sm text-gray-600"><?= htmlspecialchars((string)($shill['group_name'] ?? '-')) ?></td><td class="px-4 py-4 align-top text-sm text-gray-600"><?= htmlspecialchars((string)($shill['note'] ?? '-')) ?></td><td class="px-4 py-4 align-top"><span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $enabled ? 'bg-rose-100 text-rose-700' : 'bg-gray-100 text-gray-700' ?>"><?= $enabled ? '排除统计中' : '已停用' ?></span></td><td class="px-4 py-4 align-top text-right"><div class="flex items-center justify-end gap-2"><button type="button" class="edit-shill-btn inline-flex items-center px-3 py-1.5 rounded-lg border border-primary/20 text-primary text-xs hover:bg-primary/5" data-id="<?= (int)$shill['id'] ?>" data-group-id="<?= (int)($shill['group_id'] ?? 0) ?>" data-tg-user-id="<?= htmlspecialchars((string)($shill['tg_user_id'] ?? ''), ENT_QUOTES) ?>" data-note="<?= htmlspecialchars((string)($shill['note'] ?? ''), ENT_QUOTES) ?>"><i class="fas fa-pen mr-1"></i>编辑</button><button type="button" class="shill-toggle-btn inline-flex items-center px-3 py-1.5 rounded-lg text-xs <?= $enabled ? 'bg-red-50 text-red-600 border border-red-200' : 'bg-green-50 text-green-600 border border-green-200' ?>" data-id="<?= (int)$shill['id'] ?>" data-enabled="<?= $enabled ? 0 : 1 ?>"><?= $enabled ? '取消排除' : '重新排除' ?></button></div></td></tr><?php endforeach; else: ?><tr><td colspan="5" class="px-4 py-10 text-center text-sm text-gray-500">暂无托号配置。</td></tr><?php endif; ?></tbody></table></div>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
<div class="flex items-start justify-between gap-4 mb-4">
<div>
<h2 class="text-xl font-semibold text-gray-800">消息提醒 / 推送运营面</h2>
<p class="text-sm text-gray-500 mt-1">先把自动消息、开奖提醒、封盘倒计时、上下分提醒的运营可见性拉出来,便于对照 TG 成品补齐行为。</p>
</div>
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs bg-cyan-100 text-cyan-700"><i class="fas fa-wave-square mr-1"></i> Push Ops</span>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<h3 class="text-base font-semibold text-gray-800 mb-3">推送类型汇总</h3>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">总数</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">成功</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">失败</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">待发/跳过</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<?php foreach (($pushSummary ?? []) as $pushType => $summary): ?>
<?php if ((int)($summary['total'] ?? 0) === 0) continue; ?>
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-800"><?= htmlspecialchars((string)$pushType) ?></td>
<td class="px-4 py-3 text-gray-600"><?= (int)($summary['total'] ?? 0) ?></td>
<td class="px-4 py-3 text-emerald-600"><?= (int)($summary['success'] ?? 0) ?></td>
<td class="px-4 py-3 text-red-600"><?= (int)($summary['failed'] ?? 0) ?></td>
<td class="px-4 py-3 text-amber-600"><?= (int)($summary['pending'] ?? 0) + (int)($summary['skipped'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty(array_filter($pushSummary ?? [], static function ($item) { return (int)($item['total'] ?? 0) > 0; }))): ?>
<tr><td colspan="5" class="px-4 py-8 text-center text-sm text-gray-500">暂无推送汇总数据,待 TG runtime 写入。</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div>
<h3 class="text-base font-semibold text-gray-800 mb-3">群提醒能力快照</h3>
<div class="space-y-3">
<?php if (!empty($groups)): foreach (array_slice($groups, 0, 8) as $group): ?>
<div class="border border-gray-200 rounded-lg p-4">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-sm font-semibold text-gray-800"><?= htmlspecialchars((string)($group['group_name'] ?? '-')) ?></div>
<div class="text-xs text-gray-500 mt-1"><?= htmlspecialchars((string)($group['tg_group_id'] ?? '-')) ?></div>
</div>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= !empty($group['status']) ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600' ?>"><?= !empty($group['status']) ? '启用中' : '停用中' ?></span>
</div>
<div class="mt-3 flex flex-wrap gap-2 text-xs">
<span class="px-2 py-1 rounded-full <?= !empty($group['remind_bet_success']) ? 'bg-emerald-100 text-emerald-700' : 'bg-gray-100 text-gray-500' ?>">下注成功</span>
<span class="px-2 py-1 rounded-full <?= !empty($group['remind_draw_result']) ? 'bg-sky-100 text-sky-700' : 'bg-gray-100 text-gray-500' ?>">开奖提醒</span>
<span class="px-2 py-1 rounded-full <?= !empty($group['remind_close_countdown']) ? 'bg-amber-100 text-amber-700' : 'bg-gray-100 text-gray-500' ?>">封盘倒计时</span>
<span class="px-2 py-1 rounded-full <?= !empty($group['animation_enabled']) ? 'bg-fuchsia-100 text-fuchsia-700' : 'bg-gray-100 text-gray-500' ?>">开奖动画</span>
</div>
</div>
<?php endforeach; else: ?>
<div class="text-sm text-gray-500">暂无群配置,无法展示提醒快照。</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
<div class="flex items-start justify-between gap-4 mb-4"><div><h2 class="text-xl font-semibold text-gray-800">最近推送日志</h2><p class="text-sm text-gray-500 mt-1">覆盖 countdown / bet_success / draw / credit / debit / system,先做观测面,再回填自动触发链。</p></div></div>
<div class="overflow-x-auto"><table class="w-full text-sm"><thead class="bg-gray-50"><tr><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">群</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">期号 / 消息</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">载荷摘要</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">时间</th></tr></thead><tbody class="divide-y divide-gray-200"><?php if (!empty($pushLogs)): foreach ($pushLogs as $pushLog): ?><?php $status = (string)($pushLog['status'] ?? 'pending'); $payload = is_array($pushLog['payload'] ?? null) ? $pushLog['payload'] : []; $summaryParts = []; if (isset($payload['amount'])) { $summaryParts[] = '金额 ' . number_format((float)$payload['amount'], 2); } if (!empty($payload['balance_after'])) { $summaryParts[] = '余额 ' . number_format((float)$payload['balance_after'], 2); } if (!empty($payload['operator'])) { $summaryParts[] = '操作人 ' . (string)$payload['operator']; } if (!empty($payload['username'])) { $summaryParts[] = '用户 ' . (string)$payload['username']; } if (!empty($payload['text'])) { $summaryParts[] = mb_substr((string)$payload['text'], 0, 36); } $summaryText = !empty($summaryParts) ? implode(' / ', $summaryParts) : json_encode($payload, JSON_UNESCAPED_UNICODE); if (!$summaryText) { $summaryText = '-'; } ?><tr class="hover:bg-gray-50"><td class="px-4 py-4 align-top"><div class="font-medium text-gray-800"><?= htmlspecialchars((string)($pushLog['group_name'] ?? '未知群组')) ?></div><div class="text-xs text-gray-500 mt-1"><?= htmlspecialchars((string)($pushLog['tg_group_id'] ?? '-')) ?></div></td><td class="px-4 py-4 align-top"><span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-cyan-100 text-cyan-700"><?= htmlspecialchars((string)($pushLog['push_type'] ?? 'system')) ?></span></td><td class="px-4 py-4 align-top text-sm text-gray-600"><div>期号:<?= htmlspecialchars((string)($pushLog['period_number'] ?? '-')) ?></div><div class="text-xs text-gray-500 mt-1">TG Msg ID<?= htmlspecialchars((string)($pushLog['tg_message_id'] ?? '-')) ?></div></td><td class="px-4 py-4 align-top text-sm text-gray-600 max-w-xs break-words"><?= htmlspecialchars((string)$summaryText) ?><?php if (!empty($pushLog['error_message'])): ?><div class="text-xs text-red-500 mt-1">错误:<?= htmlspecialchars((string)$pushLog['error_message']) ?></div><?php endif; ?></td><td class="px-4 py-4 align-top"><span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $status === 'success' ? 'bg-green-100 text-green-800' : ($status === 'failed' ? 'bg-red-100 text-red-700' : ($status === 'skipped' ? 'bg-gray-100 text-gray-700' : 'bg-amber-100 text-amber-700')) ?>"><?= htmlspecialchars($status) ?></span></td><td class="px-4 py-4 align-top text-sm text-gray-500"><?= htmlspecialchars((string)($pushLog['created_at'] ?? '-')) ?></td></tr><?php endforeach; else: ?><tr><td colspan="6" class="px-4 py-10 text-center text-sm text-gray-500">暂无推送日志。</td></tr><?php endif; ?></tbody></table></div>
</div>
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
<div class="flex items-start justify-between gap-4 mb-4"><div><h2 class="text-xl font-semibold text-gray-800">Bot API 请求审计</h2><p class="text-sm text-gray-500 mt-1">对照成品 TG 机器人的发送队列与多账号路由,这里补入网站侧入站请求审计链,便于排查签名、限流、重复请求。</p></div></div>
<div class="overflow-x-auto"><table class="w-full text-sm"><thead class="bg-gray-50"><tr><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Bot / 群</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">请求</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">幂等 / IP</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">签名 / 响应</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">时间</th></tr></thead><tbody class="divide-y divide-gray-200"><?php if (!empty($apiRequestLogs)): foreach ($apiRequestLogs as $apiLog): ?><?php $signatureOk = (int)($apiLog['signature_ok'] ?? 0) === 1; ?><tr class="hover:bg-gray-50"><td class="px-4 py-4 align-top"><div class="font-medium text-gray-800"><?= htmlspecialchars((string)($apiLog['bot_name'] ?? '未知 Bot')) ?></div><div class="text-xs text-gray-500 mt-1"><?= htmlspecialchars((string)($apiLog['group_name'] ?? '-')) ?> / <?= htmlspecialchars((string)($apiLog['tg_group_id'] ?? '-')) ?></div></td><td class="px-4 py-4 align-top text-sm text-gray-600"><div><?= htmlspecialchars((string)($apiLog['http_method'] ?? 'POST')) ?></div><div class="text-xs text-gray-500 mt-1 break-all"><?= htmlspecialchars((string)($apiLog['request_uri'] ?? '-')) ?></div></td><td class="px-4 py-4 align-top text-sm text-gray-600"><div class="break-all"><?= htmlspecialchars((string)($apiLog['idempotency_key'] ?? '-')) ?></div><div class="text-xs text-gray-500 mt-1">IP: <?= htmlspecialchars((string)($apiLog['client_ip'] ?? '-')) ?></div></td><td class="px-4 py-4 align-top"><div><span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $signatureOk ? 'bg-lime-100 text-lime-700' : 'bg-red-100 text-red-700' ?>"><?= $signatureOk ? 'signature ok' : 'signature fail' ?></span></div><div class="text-xs text-gray-500 mt-2">HTTP <?= (int)($apiLog['response_code'] ?? 0) ?></div></td><td class="px-4 py-4 align-top text-sm text-gray-500"><?= htmlspecialchars((string)($apiLog['created_at'] ?? '-')) ?></td></tr><?php endforeach; else: ?><tr><td colspan="5" class="px-4 py-10 text-center text-sm text-gray-500">暂无 API 请求日志。</td></tr><?php endif; ?></tbody></table></div>
</div>
<div class="bg-white rounded-xl shadow-md p-6">
<div class="flex items-center justify-between mb-4"><div><h2 class="text-xl font-semibold text-gray-800">最近 Bot 下注同步</h2><p class="text-sm text-gray-500 mt-1">用于快速核对群消息→平台订单的同步状态。</p></div></div>
<div class="overflow-x-auto"><table class="w-full text-sm"><thead class="bg-gray-50"><tr><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">群</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用户</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">期号</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">金额 / 注数</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th><th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">时间</th></tr></thead><tbody class="divide-y divide-gray-200"><?php if (!empty($recentOrders)): foreach ($recentOrders as $order): ?><?php $ok = ($order['sync_status'] ?? '') === 'success'; $isShill = (int)($order['is_shill'] ?? 0) === 1; ?><tr class="hover:bg-gray-50 transition-colors"><td class="px-4 py-4 align-top"><div class="text-sm font-medium text-gray-800"><?= htmlspecialchars((string)($order['group_name'] ?? '未知群组')) ?></div><div class="text-xs text-gray-500 mt-1"><?= htmlspecialchars((string)($order['tg_group_id'] ?? '-')) ?></div></td><td class="px-4 py-4 align-top text-sm text-gray-600"><?= htmlspecialchars((string)($order['tg_username'] ?? '-')) ?><?php if ($isShill): ?><span class="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-rose-100 text-rose-700">托</span><?php endif; ?></td><td class="px-4 py-4 align-top text-sm text-gray-600"><?= htmlspecialchars((string)($order['period_number'] ?? '-')) ?></td><td class="px-4 py-4 align-top text-sm text-gray-600"><?= number_format((float)($order['bet_amount_total'] ?? 0), 2) ?> / <?= (int)($order['accepted_bet_count'] ?? 0) ?> 注</td><td class="px-4 py-4 align-top"><span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $ok ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-700' ?>"><span class="w-2 h-2 rounded-full mr-1 <?= $ok ? 'bg-green-500' : 'bg-red-500' ?>"></span><?= htmlspecialchars((string)($order['sync_status'] ?? 'unknown')) ?></span><?php if (!$ok && !empty($order['sync_error'])): ?><div class="text-xs text-red-500 mt-1 max-w-xs break-words"><?= htmlspecialchars((string)$order['sync_error']) ?></div><?php endif; ?></td><td class="px-4 py-4 align-top text-sm text-gray-500"><?= htmlspecialchars((string)($order['created_at'] ?? '-')) ?></td></tr><?php endforeach; else: ?><tr><td colspan="6" class="px-4 py-10 text-center text-sm text-gray-500">暂无 Bot 同步记录。</td></tr><?php endif; ?></tbody></table></div>
</div>
<script>
(function () {
const alertBox = document.getElementById('botAdminAlert');
function showAlert(success, message) {
alertBox.className = 'mb-6 rounded-lg border px-4 py-3 text-sm ' + (success ? 'border-green-200 bg-green-50 text-green-700' : 'border-red-200 bg-red-50 text-red-700');
alertBox.textContent = message;
alertBox.classList.remove('hidden');
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function setSubmitButtonState(form, isEdit) {
const submitButton = form.querySelector('button[type="submit"]');
if (!submitButton) return;
submitButton.textContent = isEdit ? submitButton.dataset.updateText : submitButton.dataset.createText;
}
function setCheckboxValue(form, name, checked) {
const input = form.querySelector('[name="' + name + '"]');
if (input) input.checked = !!checked;
}
async function requestJson(url, payload) {
const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), credentials: 'same-origin' });
return response.json();
}
async function submitManagedForm(formId, createUrl, updateUrl, resetHandler) {
const form = document.getElementById(formId);
if (!form) return;
form.addEventListener('submit', async function (event) {
event.preventDefault();
const formData = new FormData(form);
const payload = {};
for (const [key, value] of formData.entries()) payload[key] = value;
form.querySelectorAll('input[type="checkbox"]').forEach(function (checkbox) { payload[checkbox.name] = checkbox.checked ? 1 : 0; });
const isEdit = !!(payload.id && String(payload.id).trim() !== '');
try {
const result = await requestJson(isEdit ? updateUrl : createUrl, payload);
showAlert(!!result.success, result.message || '请求完成');
if (result.success) {
if (typeof resetHandler === 'function') resetHandler();
setTimeout(function () { window.location.reload(); }, 600);
}
} catch (error) {
showAlert(false, '请求失败,请稍后重试');
}
});
}
function bindToggleButtons(selector, url, key) {
document.querySelectorAll(selector).forEach(function (button) {
button.addEventListener('click', async function () {
const payload = { id: Number(button.dataset.id || 0) };
payload[key] = Number(button.dataset[key] || 0);
try {
const result = await requestJson(url, payload);
showAlert(!!result.success, result.message || '状态已更新');
if (result.success) setTimeout(function () { window.location.reload(); }, 600);
} catch (error) {
showAlert(false, '状态更新失败,请稍后重试');
}
});
});
}
function bindStatusButtons() {
document.querySelectorAll('.toggle-btn').forEach(function (button) {
button.addEventListener('click', async function () {
try {
const result = await requestJson(button.dataset.url, {
id: Number(button.dataset.id || 0),
status: Number(button.dataset.status || 0)
});
showAlert(!!result.success, result.message || '状态已更新');
if (result.success) setTimeout(function () { window.location.reload(); }, 600);
} catch (error) {
showAlert(false, '状态更新失败,请稍后重试');
}
});
});
}
function bindEditForm(formId, cancelId, buttonSelector, fieldMap, defaults) {
const form = document.getElementById(formId);
const cancelButton = document.getElementById(cancelId);
if (!form || !cancelButton) return;
function resetForm() {
form.reset();
const idInput = form.querySelector('[name="id"]');
if (idInput) idInput.value = '';
Object.keys(defaults || {}).forEach(function (key) {
const field = form.querySelector('[name="' + key + '"]');
if (!field) return;
if (field.type === 'checkbox') {
field.checked = !!defaults[key];
} else {
field.value = defaults[key];
}
});
setSubmitButtonState(form, false);
cancelButton.classList.add('hidden');
}
document.querySelectorAll(buttonSelector).forEach(function (button) {
button.addEventListener('click', function () {
Object.keys(fieldMap).forEach(function (fieldName) {
const datasetKey = fieldMap[fieldName];
const field = form.querySelector('[name="' + fieldName + '"]');
if (!field) return;
if (field.type === 'checkbox') {
field.checked = Number(button.dataset[datasetKey] || 0) === 1;
} else {
field.value = button.dataset[datasetKey] || '';
}
});
setSubmitButtonState(form, true);
cancelButton.classList.remove('hidden');
form.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
});
cancelButton.addEventListener('click', resetForm);
resetForm();
return resetForm;
}
const resetBotForm = bindEditForm('botForm', 'cancelBotEditBtn', '.edit-bot-btn', {
id: 'id', name: 'name', bot_token: 'token', bot_username: 'username', run_mode: 'runMode', webhook_url: 'webhookUrl', remark: 'remark'
}, { run_mode: 'polling' });
const resetGroupForm = bindEditForm('groupForm', 'cancelGroupEditBtn', '.edit-group-btn', {
id: 'id', bot_id: 'botId', game_id: 'gameId', bet_format_rule_id: 'ruleId', tg_group_id: 'tgGroupId', group_name: 'groupName', group_type: 'groupType', countdown_config: 'countdownConfig', remind_bet_success: 'remindBetSuccess', remind_draw_result: 'remindDrawResult', remind_close_countdown: 'remindCloseCountdown', animation_enabled: 'animationEnabled', bet_enabled: 'betEnabled'
}, { remind_bet_success: true, remind_draw_result: true, remind_close_countdown: true, animation_enabled: false, bet_enabled: true, group_type: 'group' });
const resetWalletForm = bindEditForm('walletForm', 'cancelWalletEditBtn', '.edit-wallet-btn', {
id: 'id', group_id: 'groupId', platform_user_id: 'platformUserId', wallet_mode: 'walletMode'
}, { wallet_mode: 'master_pool' });
const resetMemberForm = bindEditForm('memberForm', 'cancelMemberEditBtn', '.edit-member-btn', {
id: 'id', group_id: 'groupId', tg_user_id: 'tgUserId', tg_username: 'tgUsername', tg_nickname: 'tgNickname', platform_user_id: 'platformUserId', role: 'role', bet_enabled: 'betEnabled'
}, { role: 'member', bet_enabled: true });
const resetShillForm = bindEditForm('shillForm', 'cancelShillEditBtn', '.edit-shill-btn', {
id: 'id', group_id: 'groupId', tg_user_id: 'tgUserId', note: 'note'
}, {});
submitManagedForm('botForm', '/admin/bots/save', '/admin/bots/update', resetBotForm);
submitManagedForm('groupForm', '/admin/bots/groups/save', '/admin/bots/groups/update', resetGroupForm);
submitManagedForm('walletForm', '/admin/bots/wallets/save', '/admin/bots/wallets/update', resetWalletForm);
submitManagedForm('memberForm', '/admin/bots/members/save', '/admin/bots/members/update', resetMemberForm);
submitManagedForm('shillForm', '/admin/bots/shills/save', '/admin/bots/shills/update', resetShillForm);
bindStatusButtons();
bindToggleButtons('.member-bet-toggle-btn', '/admin/bots/members/toggle-bet', 'betEnabled');
bindToggleButtons('.shill-toggle-btn', '/admin/bots/shills/toggle', 'enabled');
})();
</script>
+152
View File
@@ -0,0 +1,152 @@
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
<i class="fas fa-clipboard-list text-primary mr-3"></i>
跟单计划管理
</h1>
<!-- 新建按钮 -->
<div class="mb-4">
<button onclick="showAddForm()" class="px-5 py-2 bg-primary text-white rounded-lg text-sm hover:opacity-90 transition">
<i class="fas fa-plus mr-1"></i> 新建计划
</button>
</div>
<!-- 计划列表 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
<div class="overflow-x-auto">
<table class="w-full bg-white rounded-xl overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">计划名称</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">类型</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">名次</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">建议金额</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">总期数</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">胜率</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">跟单人数</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">状态</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<?php if (!empty($plans)): ?>
<?php foreach ($plans as $p): ?>
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-4 py-3 text-sm text-gray-500"><?= $p['id'] ?></td>
<td class="px-4 py-3 text-sm font-medium text-gray-900"><?= htmlspecialchars($p['name']) ?></td>
<td class="px-4 py-3 text-sm text-center">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-primary/10 text-primary">
<?= ['bs'=>'大小','oe'=>'单双','dt'=>'龙虎','sum_bs'=>'冠亚大小'][$p['plan_type']] ?? $p['plan_type'] ?>
</span>
</td>
<td class="px-4 py-3 text-sm text-center"><?= $p['target_rank'] ?></td>
<td class="px-4 py-3 text-sm text-right"><?= number_format($p['bet_amount'], 2) ?></td>
<td class="px-4 py-3 text-sm text-center"><?= $p['total_records'] ?></td>
<td class="px-4 py-3 text-sm text-center">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $p['win_rate'] >= 50 ? 'bg-success/10 text-success' : 'bg-danger/10 text-danger' ?>">
<?= $p['win_rate'] ?>%
</span>
</td>
<td class="px-4 py-3 text-sm text-center font-semibold"><?= $p['follower_count'] ?></td>
<td class="px-4 py-3 text-sm text-center">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs cursor-pointer <?= $p['status'] ? 'bg-success/10 text-success' : 'bg-gray-200 text-gray-500' ?>"
onclick="togglePlan(<?= $p['id'] ?>)">
<?= $p['status'] ? '启用' : '禁用' ?>
</span>
</td>
<td class="px-4 py-3 text-sm text-center">
<button onclick='editPlan(<?= json_encode($p) ?>)' class="text-primary hover:underline text-xs mr-2">编辑</button>
<button onclick="deletePlan(<?= $p['id'] ?>)" class="text-danger hover:underline text-xs">删除</button>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="10" class="px-6 py-12 text-center">
<div class="flex flex-col items-center">
<i class="fas fa-clipboard-list text-gray-300 text-5xl mb-4"></i>
<h3 class="text-lg font-medium text-gray-900">暂无跟单计划</h3>
<p class="mt-1 text-gray-500 text-sm">点击上方按钮新建跟单计划</p>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<script src="/Static/js/admin.js"></script>
<script>
var planTypes = {bs:'大小', oe:'单双', dt:'龙虎', sum_bs:'冠亚大小'};
function showAddForm(data) {
var isEdit = data && data.id;
var html = '<div style="padding:20px">' +
'<div style="margin-bottom:12px"><label style="display:block;margin-bottom:4px;font-size:13px;color:#666">计划名称</label>' +
'<input type="text" id="fp_name" value="' + (data ? data.name : '') + '" class="layui-input" style="width:100%"></div>' +
'<div style="display:flex;gap:12px;margin-bottom:12px">' +
'<div style="flex:1"><label style="display:block;margin-bottom:4px;font-size:13px;color:#666">类型</label>' +
'<select id="fp_type" class="layui-input">' +
'<option value="bs"' + (data && data.plan_type === 'bs' ? ' selected' : '') + '>大小</option>' +
'<option value="oe"' + (data && data.plan_type === 'oe' ? ' selected' : '') + '>单双</option>' +
'<option value="dt"' + (data && data.plan_type === 'dt' ? ' selected' : '') + '>龙虎</option>' +
'<option value="sum_bs"' + (data && data.plan_type === 'sum_bs' ? ' selected' : '') + '>冠亚大小</option>' +
'</select></div>' +
'<div style="flex:1"><label style="display:block;margin-bottom:4px;font-size:13px;color:#666">目标名次</label>' +
'<input type="number" id="fp_rank" min="1" max="10" value="' + (data ? data.target_rank : 1) + '" class="layui-input"></div>' +
'</div>' +
'<div style="margin-bottom:12px"><label style="display:block;margin-bottom:4px;font-size:13px;color:#666">建议投注金额</label>' +
'<input type="number" id="fp_amount" value="' + (data ? data.bet_amount : 100) + '" class="layui-input"></div>' +
'</div>';
layui.use('layer', function() {
layui.layer.open({
type: 1, title: isEdit ? '编辑计划' : '新建计划',
area: ['420px', '360px'], content: html,
btn: ['保存', '取消'],
yes: function(index) {
var postData = {
name: document.getElementById('fp_name').value,
plan_type: document.getElementById('fp_type').value,
target_rank: document.getElementById('fp_rank').value,
bet_amount: document.getElementById('fp_amount').value,
game_id: 1, status: 1
};
if (isEdit) postData.id = data.id;
fetch('/admin/follow-plans/save', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify(postData)
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) { layui.layer.close(index); location.reload(); }
else { layui.layer.msg(d.message || '保存失败'); }
});
}
});
});
}
function editPlan(plan) { showAddForm(plan); }
function togglePlan(id) {
fetch('/admin/follow-plans/toggle', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: id})
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) location.reload();
});
}
function deletePlan(id) {
layui.use('layer', function() {
layui.layer.confirm('确定删除此计划?所有相关记录将被清除。', function(index) {
fetch('/admin/follow-plans/delete', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: id})
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) { layui.layer.close(index); location.reload(); }
});
});
});
}
</script>
+306
View File
@@ -0,0 +1,306 @@
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
<i class="fas fa-file-invoice-dollar text-primary mr-3"></i>
充提审核
</h1>
<!-- 统计卡片 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">待处理</p>
<h3 class="text-xl font-bold mt-1 text-warning" id="statPending">
<?= (int)($pendingCount ?? 0) ?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-warning/10 flex items-center justify-center">
<i class="fas fa-clock text-warning"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">今日充值申请</p>
<h3 class="text-xl font-bold mt-1 text-success">
<?= (int)($todayDeposit ?? 0) ?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
<i class="fas fa-arrow-down text-success"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">今日提现申请</p>
<h3 class="text-xl font-bold mt-1 text-danger">
<?= (int)($todayWithdraw ?? 0) ?>
</h3>
</div>
<div class="w-10 h-10 rounded-full bg-danger/10 flex items-center justify-center">
<i class="fas fa-arrow-up text-danger"></i>
</div>
</div>
</div>
</div>
<!-- 筛选栏 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
<div class="flex flex-col md:flex-row md:items-center gap-4">
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600 whitespace-nowrap">类型:</label>
<select id="filterType" class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="">全部</option>
<option value="deposit">充值</option>
<option value="withdraw">提现</option>
</select>
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600 whitespace-nowrap">状态:</label>
<select id="filterStatus" class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="">全部</option>
<option value="pending">待处理</option>
<option value="approved">已通过</option>
<option value="rejected">已拒绝</option>
</select>
</div>
<button id="btnFilter" class="px-4 py-2 bg-primary text-white rounded-lg text-sm hover:bg-primary/90 transition-colors">
<i class="fas fa-search mr-1"></i>筛选
</button>
</div>
</div>
<!-- 申请列表 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
<div>
<h2 class="text-xl font-semibold text-gray-800">充提申请列表</h2>
<p class="text-sm text-gray-500 mt-1">审核用户的充值和提现申请</p>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full bg-white rounded-xl overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">用户</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">钱包地址</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">金额</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">用户备注</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">管理员备注</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider hidden lg:table-cell">创建时间</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200" id="requestList">
<?php if (!empty($requests) && is_array($requests)): ?>
<?php foreach ($requests as $req): ?>
<tr class="hover:bg-gray-50 transition-colors fund-row"
data-type="<?= htmlspecialchars((string)($req['type'] ?? '')) ?>"
data-status="<?= htmlspecialchars((string)($req['status'] ?? '')) ?>">
<td class="px-4 py-4">
<span class="text-sm font-medium text-gray-900">#<?= htmlspecialchars((string)($req['id'] ?? '')) ?></span>
</td>
<td class="px-4 py-4">
<div class="text-sm text-gray-900"><?= htmlspecialchars((string)($req['username'] ?? '未知')) ?></div>
<div class="text-xs text-gray-500">ID: <?= htmlspecialchars((string)($req['user_id'] ?? '')) ?></div>
</td>
<td class="px-4 py-4">
<?php $addr = $req['usdt_address'] ?? ''; ?>
<?php if ($addr): ?>
<div class="text-xs text-gray-700 font-mono max-w-[160px] truncate cursor-pointer" title="<?= htmlspecialchars($addr) ?>" onclick="navigator.clipboard.writeText('<?= htmlspecialchars($addr) ?>');layui.layer&&layui.layer.msg('已复制')">
<?= htmlspecialchars($addr) ?>
</div>
<?php else: ?>
<span class="text-xs text-gray-400">未绑定</span>
<?php endif; ?>
</td>
<td class="px-4 py-4">
<?php if (($req['type'] ?? '') === 'deposit'): ?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-success/10 text-success">充值</span>
<?php else: ?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-danger/10 text-danger">提现</span>
<?php endif; ?>
</td>
<td class="px-4 py-4">
<span class="text-sm font-semibold text-gray-900">
<?= number_format(floatval($req['amount'] ?? 0), 2) ?>
</span>
</td>
<td class="px-4 py-4">
<?php
$status = $req['status'] ?? '';
$statusMap = [
'pending' => ['text' => '待处理', 'class' => 'bg-warning/10 text-warning'],
'approved' => ['text' => '已通过', 'class' => 'bg-success/10 text-success'],
'rejected' => ['text' => '已拒绝', 'class' => 'bg-danger/10 text-danger'],
];
$statusInfo = $statusMap[$status] ?? ['text' => $status, 'class' => 'bg-gray-100 text-gray-700'];
?>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $statusInfo['class'] ?>">
<?= $statusInfo['text'] ?>
</span>
</td>
<td class="px-4 py-4 hidden md:table-cell">
<span class="text-sm text-gray-600 max-w-[120px] truncate block">
<?= htmlspecialchars((string)($req['remark'] ?? '-')) ?>
</span>
</td>
<td class="px-4 py-4 hidden md:table-cell">
<span class="text-sm text-gray-600 max-w-[120px] truncate block">
<?= htmlspecialchars((string)($req['admin_remark'] ?? '-')) ?>
</span>
</td>
<td class="px-4 py-4 hidden lg:table-cell">
<?php if (!empty($req['created_at'])): ?>
<div class="text-xs text-gray-500">
<?= date('Y-m-d H:i:s', strtotime((string)$req['created_at'])) ?>
</div>
<?php else: ?>
<span class="text-xs text-gray-400">-</span>
<?php endif; ?>
</td>
<td class="px-4 py-4">
<?php if ($status === 'pending'): ?>
<div class="flex gap-2">
<button onclick="approveRequest(<?= (int)$req['id'] ?>)"
class="px-3 py-1 bg-success text-white text-xs rounded-lg hover:bg-success/90 transition-colors">
<i class="fas fa-check mr-1"></i>通过
</button>
<button onclick="rejectRequest(<?= (int)$req['id'] ?>)"
class="px-3 py-1 bg-danger text-white text-xs rounded-lg hover:bg-danger/90 transition-colors">
<i class="fas fa-times mr-1"></i>拒绝
</button>
</div>
<?php else: ?>
<span class="text-xs text-gray-400">已处理</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="10" class="px-6 py-12 text-center">
<div class="flex flex-col items-center">
<i class="fas fa-file-invoice text-gray-300 text-5xl mb-4"></i>
<h3 class="text-lg font-medium text-gray-900">暂无充提申请</h3>
<p class="mt-1 text-gray-500 text-sm">还没有任何充提申请记录。</p>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<script src="/Static/js/admin.js"></script>
<script>
layui.use(['layer'], function() {
var layer = layui.layer;
// --- 筛选逻辑 ---
document.getElementById('btnFilter').addEventListener('click', function() {
var typeVal = document.getElementById('filterType').value;
var statusVal = document.getElementById('filterStatus').value;
var rows = document.querySelectorAll('.fund-row');
rows.forEach(function(row) {
var show = true;
if (typeVal && row.dataset.type !== typeVal) show = false;
if (statusVal && row.dataset.status !== statusVal) show = false;
row.style.display = show ? '' : 'none';
});
});
// --- 审批通过 ---
window.approveRequest = function(id) {
layer.prompt({title: '审批通过 - 管理员备注(可留空)', formType: 2, value: ''}, function(remark, promptIndex) {
layer.close(promptIndex);
var loadIdx = layer.load(1);
fetch('/admin/fund-requests/approve', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: id, admin_remark: remark})
})
.then(function(r){ return r.json(); })
.then(function(d){
layer.close(loadIdx);
if(d.success){
layer.msg('审批通过', {icon: 1});
setTimeout(function(){ location.reload(); }, 800);
} else {
layer.msg(d.message || '操作失败', {icon: 2});
}
})
.catch(function(){
layer.close(loadIdx);
layer.msg('网络错误', {icon: 2});
});
});
};
// --- 拒绝 ---
window.rejectRequest = function(id) {
layer.prompt({title: '拒绝原因(可留空)', formType: 2, value: ''}, function(remark, promptIndex) {
layer.close(promptIndex);
var loadIdx = layer.load(1);
fetch('/admin/fund-requests/reject', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: id, admin_remark: remark})
})
.then(function(r){ return r.json(); })
.then(function(d){
layer.close(loadIdx);
if(d.success){
layer.msg('已拒绝', {icon: 1});
setTimeout(function(){ location.reload(); }, 800);
} else {
layer.msg(d.message || '操作失败', {icon: 2});
}
})
.catch(function(){
layer.close(loadIdx);
layer.msg('网络错误', {icon: 2});
});
});
};
// --- 语音提醒 + 标题闪烁轮询 ---
var lastPendingCount = <?= (int)($pendingCount ?? 0) ?>;
var originalTitle = document.title;
setInterval(function() {
fetch('/admin/fund-requests/pending-count')
.then(function(r){ return r.json(); })
.then(function(d){
var count = d.count || 0;
document.getElementById('statPending').textContent = count;
if (count > lastPendingCount) {
// 播放短促提示音
try {
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var osc = audioCtx.createOscillator();
var gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.frequency.value = 880;
osc.type = 'sine';
gain.gain.value = 0.3;
osc.start();
osc.stop(audioCtx.currentTime + 0.15);
} catch(e){}
// 标题闪烁
document.title = '⚠️ 新充提申请(' + count + ')';
setTimeout(function(){ document.title = originalTitle; }, 3000);
}
lastPendingCount = count;
})
.catch(function(){});
}, 15000);
});
</script>
+37 -1
View File
@@ -36,6 +36,13 @@
<a href="/admin/dashboard" class="flex items-center text-primary font-bold text-xl">
<?php
$adminSettings = \App\Core\SettingsHelper::getAll();
// 查询充提待处理数量(侧边栏角标用)
try {
$_fundDb = new \Db\Database();
$pendingFundCount = $_fundDb->count('fund_requests', ['status' => 'pending']);
} catch (\Throwable $e) {
$pendingFundCount = 0;
}
?>
<img src="<?= htmlspecialchars($adminSettings['site_logo'] ?? '/Static/tm68/logo_tm68.png.png') ?>" class="w-10 mr-2">
<span>管理后台</span>
@@ -116,6 +123,11 @@
<i class="fas fa-list-alt w-5 text-center"></i>
<span>投注记录</span>
</a>
<a href="/admin/fund-requests" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item relative">
<i class="fas fa-file-invoice-dollar w-5 text-center"></i>
<span>充提审核</span>
<span id="fundRequestBadge" class="absolute right-2 top-1/2 -translate-y-1/2 bg-danger text-white text-xs rounded-full min-w-[20px] h-5 flex items-center justify-center px-1" style="<?= ((int)($pendingFundCount ?? 0)) > 0 ? '' : 'display:none' ?>"><?= (int)($pendingFundCount ?? 0) ?></span>
</a>
<a href="/admin/finance" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-dollar-sign w-5 text-center"></i>
<span>财务管理</span>
@@ -136,6 +148,14 @@
<i class="fas fa-chart-bar w-5 text-center"></i>
<span>数据报表</span>
</a>
<a href="/admin/follow-plans" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-clipboard-list w-5 text-center"></i>
<span>跟单计划</span>
</a>
<a href="/admin/bots" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-robot w-5 text-center"></i>
<span>Bot 管理</span>
</a>
<a href="/admin/settings" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 hover:bg-gray-light main-menu-item">
<i class="fas fa-cog w-5 text-center"></i>
<span>系统设置</span>
@@ -265,7 +285,7 @@
<footer class="bg-white border-t border-gray-200 py-4 md:ml-64 transition-all duration-300">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col md:flex-row justify-between items-center">
<p class="text-sm text-gray-500"> &copy; <?=date('Y')?> PK10 极速赛车后台管理系统 </p>
<p class="text-sm text-gray-500"> &copy; <?=date('Y')?> F1赛车后台管理系统 </p>
</div>
</div>
</footer>
@@ -274,6 +294,22 @@
<script> const targetUsername = '<?= htmlspecialchars($_SESSION[' username '] ?? ' ') ?>'; </script>
<script src="/Static/js/controller.js"> </script>
<script src="/Static/js/admin.js"> </script>
<script>
// 全局轮询充提待处理角标
(function(){
var badge = document.getElementById('fundRequestBadge');
if(!badge) return;
setInterval(function(){
fetch('/admin/fund-requests/pending-count')
.then(function(r){return r.json();})
.then(function(d){
var c = d.count || 0;
badge.textContent = c;
badge.style.display = c > 0 ? '' : 'none';
}).catch(function(){});
}, 15000);
})();
</script>
</body>
</html>
+395 -52
View File
@@ -1,60 +1,403 @@
<div class="p-6">
<h2 class="text-2xl font-bold mb-4">📊 数据报表</h2>
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
<i class="fas fa-chart-bar text-primary mr-3"></i>
数据报表
</h1>
<!-- 日期筛选 -->
<div class="bg-white rounded-xl p-4 shadow mb-6 flex items-center gap-4 flex-wrap">
<label class="text-sm text-gray-400">起始:</label><input type="date" value="<?=$dateFrom?>" id="dateFrom" class="border rounded px-3 py-2 text-sm">
<label class="text-sm text-gray-400">截止:</label><input type="date" value="<?=$dateTo?>" id="dateTo" class="border rounded px-3 py-2 text-sm">
<button onclick="location.href='/admin/reports?from='+document.getElementById('dateFrom').value+'&to='+document.getElementById('dateTo').value" class="px-4 py-2 bg-blue-500 text-white rounded text-sm">筛选</button>
<!-- 日期筛选 -->
<div class="bg-white rounded-xl p-5 card-shadow mb-6">
<div class="flex flex-wrap items-center gap-4">
<div class="flex items-center gap-2">
<label class="text-sm text-gray-500">起始日期</label>
<input type="text" id="dateFrom" value="<?= htmlspecialchars($dateFrom) ?>" class="border border-gray-300 rounded-lg px-3 py-2 text-sm w-36" readonly placeholder="选择日期">
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-500">截止日期</label>
<input type="text" id="dateTo" value="<?= htmlspecialchars($dateTo) ?>" class="border border-gray-300 rounded-lg px-3 py-2 text-sm w-36" readonly placeholder="选择日期">
</div>
<button onclick="doQuery()" class="px-5 py-2 bg-primary text-white rounded-lg text-sm hover:opacity-90 transition">
<i class="fas fa-search mr-1"></i> 查询
</button>
<button onclick="doExport()" class="px-5 py-2 bg-success text-white rounded-lg text-sm hover:opacity-90 transition">
<i class="fas fa-file-csv mr-1"></i> 导出CSV
</button>
</div>
</div>
<!-- 卡片 -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">总投注</div><div class="text-xl font-bold text-blue-500"><?=number_format($totalBet,2)?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">总中奖</div><div class="text-xl font-bold text-red-500"><?=number_format($totalWin,2)?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">平台盈利</div><div class="text-xl font-bold <?=$profit>=0?'text-green-500':'text-red-500'?>"><?=number_format($profit,2)?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">佣金</div><div class="text-xl font-bold text-orange-500"><?=number_format($totalCommission,2)?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">总充值</div><div class="text-xl font-bold text-green-500"><?=number_format($totalDeposit,2)?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">总提现</div><div class="text-xl font-bold text-red-500"><?=number_format($totalWithdraw,2)?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">总用户</div><div class="text-xl font-bold"><?=$totalUsers?></div></div>
<div class="bg-white rounded-xl p-4 shadow"><div class="text-gray-400 text-xs">新增用户</div><div class="text-xl font-bold text-blue-500"><?=$newUsers?></div></div>
<!-- 体统计卡片 -->
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-4 mb-6">
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">总投注</p>
<h3 class="text-xl font-bold mt-1 text-warning"><?= number_format((float)$totalBet, 2) ?></h3>
</div>
<div class="w-10 h-10 rounded-full bg-warning/10 flex items-center justify-center">
<i class="fas fa-coins text-warning"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">总派彩</p>
<h3 class="text-xl font-bold mt-1 text-primary"><?= number_format((float)$totalWin, 2) ?></h3>
</div>
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<i class="fas fa-trophy text-primary"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">总利润</p>
<h3 class="text-xl font-bold mt-1 <?= $profit >= 0 ? 'text-success' : 'text-danger' ?>"><?= number_format((float)$profit, 2) ?></h3>
</div>
<div class="w-10 h-10 rounded-full <?= $profit >= 0 ? 'bg-success/10' : 'bg-danger/10' ?> flex items-center justify-center">
<i class="fas fa-chart-line <?= $profit >= 0 ? 'text-success' : 'text-danger' ?>"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">利润率</p>
<?php
$prColor = 'text-warning';
if ($profitRate > 10) $prColor = 'text-success';
elseif ($profitRate < 0) $prColor = 'text-danger';
?>
<h3 class="text-xl font-bold mt-1 <?= $prColor ?>"><?= $profitRate ?>%</h3>
</div>
<div class="w-10 h-10 rounded-full bg-warning/10 flex items-center justify-center">
<i class="fas fa-percentage text-warning"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">总充值</p>
<h3 class="text-xl font-bold mt-1 text-success"><?= number_format((float)$totalDeposit, 2) ?></h3>
</div>
<div class="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center">
<i class="fas fa-arrow-down text-success"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">总提现</p>
<h3 class="text-xl font-bold mt-1 text-danger"><?= number_format((float)$totalWithdraw, 2) ?></h3>
</div>
<div class="w-10 h-10 rounded-full bg-danger/10 flex items-center justify-center">
<i class="fas fa-arrow-up text-danger"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">活跃用户</p>
<h3 class="text-xl font-bold mt-1 text-primary"><?= $activeUsers ?></h3>
</div>
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<i class="fas fa-user-check text-primary"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-5 card-shadow hover-lift">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">新增用户</p>
<h3 class="text-xl font-bold mt-1 text-secondary"><?= $newUsers ?></h3>
</div>
<div class="w-10 h-10 rounded-full bg-secondary/10 flex items-center justify-center">
<i class="fas fa-user-plus text-secondary"></i>
</div>
</div>
</div>
</div>
<!-- 日明细 -->
<div class="bg-white rounded-xl shadow overflow-hidden mb-6">
<h3 class="font-bold p-4 border-b">每日明细</h3>
<table class="w-full text-sm">
<thead class="bg-gray-50"><tr><th class="px-4 py-2 text-left">日期</th><th class="px-4 py-2">投注</th><th class="px-4 py-2">中奖</th><th class="px-4 py-2">盈利</th><th class="px-4 py-2">充值</th><th class="px-4 py-2">提现</th></tr></thead>
<tbody>
<?php foreach($dailyStats??[] as $d): $dp=$d['bets']-$d['wins']; ?>
<tr class="border-t hover:bg-gray-50">
<td class="px-4 py-2"><?=$d['date']?></td>
<td class="px-4 py-2 text-center"><?=number_format($d['bets'],2)?></td>
<td class="px-4 py-2 text-center"><?=number_format($d['wins'],2)?></td>
<td class="px-4 py-2 text-center <?=$dp>=0?'text-green-500':'text-red-500'?>"><?=number_format($dp,2)?></td>
<td class="px-4 py-2 text-center"><?=number_format($d['deposits'],2)?></td>
<td class="px-4 py-2 text-center"><?=number_format($d['withdraws'],2)?></td>
</tr>
<?php endforeach; ?>
</tbody></table></div>
<!-- 日明细 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-calendar-alt text-primary mr-2"></i> 日明细统计
</h2>
<div class="overflow-x-auto">
<table class="w-full bg-white rounded-xl overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">日期</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">投注额</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">派彩额</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">利润</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">充值</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">提现</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">笔数</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">胜率</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<?php if (!empty($dailyStats)): ?>
<?php foreach ($dailyStats as $d): ?>
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-4 py-3 text-sm font-medium text-gray-900"><?= $d['date'] ?></td>
<td class="px-4 py-3 text-sm text-right"><?= number_format($d['total_bet'], 2) ?></td>
<td class="px-4 py-3 text-sm text-right"><?= number_format($d['total_win'], 2) ?></td>
<td class="px-4 py-3 text-sm text-right font-semibold <?= $d['profit'] >= 0 ? 'text-success' : 'text-danger' ?>">
<?= $d['profit'] >= 0 ? '+' : '' ?><?= number_format($d['profit'], 2) ?>
</td>
<td class="px-4 py-3 text-sm text-right text-success"><?= number_format($d['deposits'], 2) ?></td>
<td class="px-4 py-3 text-sm text-right text-danger"><?= number_format($d['withdraws'], 2) ?></td>
<td class="px-4 py-3 text-sm text-center"><?= $d['bet_count'] ?></td>
<td class="px-4 py-3 text-sm text-center">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $d['win_rate'] > 50 ? 'bg-danger/10 text-danger' : 'bg-success/10 text-success' ?>">
<?= $d['win_rate'] ?>%
</span>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="8" class="px-6 py-12 text-center">
<div class="flex flex-col items-center">
<i class="fas fa-chart-bar text-gray-300 text-5xl mb-4"></i>
<h3 class="text-lg font-medium text-gray-900">暂无数据</h3>
<p class="mt-1 text-gray-500 text-sm">选定日期范围内没有投注记录</p>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- 用户投注排行 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-ranking-star text-warning mr-2"></i> 用户投注排行(前20
</h2>
<div class="overflow-x-auto">
<table class="w-full bg-white rounded-xl overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase w-16">排名</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">用户名</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">投注额</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">派彩额</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">平台盈亏</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">笔数</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<?php if (!empty($topUsers)): ?>
<?php $rank = 1; foreach ($topUsers as $u): ?>
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-4 py-3 text-center">
<?php if ($rank <= 3): ?>
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full text-white text-xs font-bold
<?= $rank === 1 ? 'bg-yellow-500' : ($rank === 2 ? 'bg-gray-400' : 'bg-amber-600') ?>">
<?= $rank ?>
</span>
<?php else: ?>
<span class="text-sm text-gray-500"><?= $rank ?></span>
<?php endif; ?>
</td>
<td class="px-4 py-3 text-sm font-medium text-gray-900"><?= htmlspecialchars($u['username'] ?? '') ?></td>
<td class="px-4 py-3 text-sm text-right"><?= number_format(floatval($u['total_bet']), 2) ?></td>
<td class="px-4 py-3 text-sm text-right"><?= number_format(floatval($u['total_win']), 2) ?></td>
<td class="px-4 py-3 text-sm text-right font-semibold <?= floatval($u['profit']) >= 0 ? 'text-success' : 'text-danger' ?>">
<?= floatval($u['profit']) >= 0 ? '+' : '' ?><?= number_format(floatval($u['profit']), 2) ?>
</td>
<td class="px-4 py-3 text-sm text-center"><?= intval($u['bet_count']) ?></td>
</tr>
<?php $rank++; endforeach; ?>
<?php else: ?>
<tr>
<td colspan="6" class="px-6 py-12 text-center">
<div class="flex flex-col items-center">
<i class="fas fa-users text-gray-300 text-5xl mb-4"></i>
<h3 class="text-lg font-medium text-gray-900">暂无数据</h3>
<p class="mt-1 text-gray-500 text-sm">选定日期范围内没有用户投注</p>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- 代理报表 -->
<?php if(!empty($agentStats)): ?>
<div class="bg-white rounded-xl shadow overflow-hidden">
<h3 class="font-bold p-4 border-b">代理报表</h3>
<table class="w-full text-sm">
<thead class="bg-gray-50"><tr><th class="px-4 py-2 text-left">代理</th><th class="px-4 py-2">玩家数</th><th class="px-4 py-2">投注</th><th class="px-4 py-2">中奖</th><th class="px-4 py-2">佣金</th><th class="px-4 py-2">盈利</th></tr></thead>
<tbody>
<?php foreach($agentStats as $as): ?>
<tr class="border-t hover:bg-gray-50">
<td class="px-4 py-2"><?=htmlspecialchars($as['user']['username']??'')?></td>
<td class="px-4 py-2 text-center"><?=$as['players']?></td>
<td class="px-4 py-2 text-center"><?=number_format($as['bets'],2)?></td>
<td class="px-4 py-2 text-center"><?=number_format($as['wins'],2)?></td>
<td class="px-4 py-2 text-center text-orange-500"><?=number_format($as['commission'],2)?></td>
<td class="px-4 py-2 text-center <?=$as['profit']>=0?'text-green-500':'text-red-500'?>"><?=number_format($as['profit'],2)?></td>
</tr>
<?php endforeach; ?>
</tbody></table></div>
<?php endif; ?>
<?php if (!empty($agentStats)): ?>
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-user-tie text-secondary mr-2"></i> 代理报表
</h2>
<div class="overflow-x-auto">
<table class="w-full bg-white rounded-xl overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">代理</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">玩家数</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">投注额</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">中奖额</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">佣金</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">盈利</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<?php foreach ($agentStats as $as): ?>
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-4 py-3 text-sm font-medium text-gray-900"><?= htmlspecialchars($as['user']['username'] ?? '') ?></td>
<td class="px-4 py-3 text-sm text-center"><?= $as['players'] ?></td>
<td class="px-4 py-3 text-sm text-right"><?= number_format($as['bets'], 2) ?></td>
<td class="px-4 py-3 text-sm text-right"><?= number_format($as['wins'], 2) ?></td>
<td class="px-4 py-3 text-sm text-right text-warning"><?= number_format($as['commission'], 2) ?></td>
<td class="px-4 py-3 text-sm text-right font-semibold <?= $as['profit'] >= 0 ? 'text-success' : 'text-danger' ?>">
<?= $as['profit'] >= 0 ? '+' : '' ?><?= number_format($as['profit'], 2) ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<!-- 用户输赢明细 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-money-bill-trend-up text-danger mr-2"></i> 用户输赢明细
</h2>
<div class="overflow-x-auto">
<table class="w-full bg-white rounded-xl overflow-hidden" id="userWinLossTable">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">用户名</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">当前余额</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">总投注</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">总派彩</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">平台盈亏</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">笔数</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">胜率</th>
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">日明细</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<?php if (!empty($userSummary)): ?>
<?php foreach ($userSummary as $uid => $us):
$uProfit = $us['total_bet'] - $us['total_win'];
$uWinRate = $us['bet_count'] > 0 ? round($us['win_count'] / $us['bet_count'] * 100, 1) : 0;
?>
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-4 py-3 text-sm font-medium text-gray-900"><?= htmlspecialchars($us['username']) ?></td>
<td class="px-4 py-3 text-sm text-right"><?= number_format($us['balance'], 2) ?></td>
<td class="px-4 py-3 text-sm text-right"><?= number_format($us['total_bet'], 2) ?></td>
<td class="px-4 py-3 text-sm text-right"><?= number_format($us['total_win'], 2) ?></td>
<td class="px-4 py-3 text-sm text-right font-semibold <?= $uProfit >= 0 ? 'text-success' : 'text-danger' ?>">
<?= $uProfit >= 0 ? '+' : '' ?><?= number_format($uProfit, 2) ?>
</td>
<td class="px-4 py-3 text-sm text-center"><?= $us['bet_count'] ?></td>
<td class="px-4 py-3 text-sm text-center">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs <?= $uWinRate > 50 ? 'bg-danger/10 text-danger' : 'bg-success/10 text-success' ?>">
<?= $uWinRate ?>%
</span>
</td>
<td class="px-4 py-3 text-sm text-center">
<button onclick="toggleDays('days_<?= $uid ?>')" class="text-primary hover:underline text-xs">
<i class="fas fa-chevron-down"></i> 展开
</button>
</td>
</tr>
<tr id="days_<?= $uid ?>" style="display:none" class="bg-gray-50/50">
<td colspan="8" class="px-6 py-2">
<table class="w-full text-xs">
<thead><tr class="text-gray-400">
<th class="py-1 text-left">日期</th>
<th class="py-1 text-right">投注</th>
<th class="py-1 text-right">派彩</th>
<th class="py-1 text-right">盈亏</th>
<th class="py-1 text-center">笔数</th>
</tr></thead>
<tbody>
<?php foreach ($us['days'] as $day): ?>
<tr class="border-t border-gray-100">
<td class="py-1 text-gray-600"><?= $day['date'] ?></td>
<td class="py-1 text-right"><?= number_format($day['bet'], 2) ?></td>
<td class="py-1 text-right"><?= number_format($day['win'], 2) ?></td>
<td class="py-1 text-right font-semibold <?= $day['profit'] >= 0 ? 'text-success' : 'text-danger' ?>">
<?= $day['profit'] >= 0 ? '+' : '' ?><?= number_format($day['profit'], 2) ?>
</td>
<td class="py-1 text-center"><?= $day['count'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="8" class="px-6 py-12 text-center">
<div class="flex flex-col items-center">
<i class="fas fa-money-bill text-gray-300 text-5xl mb-4"></i>
<h3 class="text-lg font-medium text-gray-900">暂无数据</h3>
<p class="mt-1 text-gray-500 text-sm">选定日期范围内没有用户投注记录</p>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<script src="/Static/js/admin.js"></script>
<script>
layui.use(['laydate', 'layer'], function() {
var laydate = layui.laydate;
laydate.render({
elem: '#dateFrom',
type: 'date',
format: 'yyyy-MM-dd',
value: '<?= htmlspecialchars($dateFrom) ?>',
max: '<?= htmlspecialchars($dateTo) ?>'
});
laydate.render({
elem: '#dateTo',
type: 'date',
format: 'yyyy-MM-dd',
value: '<?= htmlspecialchars($dateTo) ?>',
max: 0
});
});
function doQuery() {
var from = document.getElementById('dateFrom').value;
var to = document.getElementById('dateTo').value;
window.location.href = '/admin/reports?from=' + from + '&to=' + to;
}
function toggleDays(id) {
var el = document.getElementById(id);
if (!el) return;
el.style.display = el.style.display === 'none' ? '' : 'none';
var btn = el.previousElementSibling.querySelector('button');
if (btn) {
var isOpen = el.style.display !== 'none';
btn.innerHTML = isOpen ? '<i class="fas fa-chevron-up"></i> 收起' : '<i class="fas fa-chevron-down"></i> 展开';
}
}
function doExport() {
var from = document.getElementById('dateFrom').value;
var to = document.getElementById('dateTo').value;
window.location.href = '/admin/reports/export?from=' + from + '&to=' + to;
}
</script>
+18
View File
@@ -99,6 +99,24 @@
</div>
</div>
<!-- 客服设置 -->
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-headset text-primary mr-2"></i>
客服设置
</h3>
<div class="space-y-4">
<div>
<label for="customer_service_url" class="block text-sm font-medium text-gray-700 mb-1">客服链接</label>
<input type="text" id="customer_service_url" name="customer_service_url"
value="<?= htmlspecialchars($settings['customer_service_url'] ?? '') ?>"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="请输入客服链接,如 https://t.me/your_support">
<p class="text-xs text-gray-500 mt-1">用户充值时将跳转至此链接联系客服,支持 Telegram、WhatsApp 等链接</p>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex justify-end gap-3 pt-4">
<button type="button" onclick="resetSettings()"
+19
View File
@@ -2,6 +2,19 @@
<h2 class="text-2xl font-bold mb-2">🌊 放水控制 & 投注限额</h2>
<p class="text-gray-500 text-sm mb-6">放水 = 控制玩家胜率。百分比越低,平台赢得越多。设为50%表示公平对赌。</p>
<!-- 平台盈利率目标 -->
<div class="bg-white rounded-xl p-4 shadow mb-6">
<h3 class="font-bold mb-2">🎯 平台盈利率目标</h3>
<div style="display:flex;align-items:center;gap:15px;flex-wrap:wrap">
<span>目标盈利率:</span>
<input type="number" id="targetProfitRate" value="<?=htmlspecialchars($targetProfitRate ?? '15')?>"
min="0" max="100" step="1" class="w-24 border rounded px-2 py-1 text-sm text-center">
<span>%</span>
<button onclick="saveProfitRate()" class="px-4 py-1 bg-purple-500 text-white rounded hover:bg-purple-600 text-sm">保存</button>
<span style="color:#999;font-size:12px">说明:0=取最大利润(原逻辑),15=平台每期目标盈利15%</span>
</div>
</div>
<?php
// 投注类型中文映射
$betTypeLabels = [
@@ -87,6 +100,12 @@ function getBetLabel($type, $labels) {
</div>
<script>
async function saveProfitRate(){
const rate=parseFloat(document.getElementById('targetProfitRate').value);
if(isNaN(rate)||rate<0||rate>100){alert('请输入0-100之间的数值');return;}
const r=await fetch('/admin/water/profit-rate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({target_profit_rate:rate})});
const d=await r.json();alert(d.status==='success'?'盈利率目标保存成功!':d.message);
}
async function saveWater(){
const items=[];
document.querySelectorAll('.water-pct').forEach(el=>{