Files
pk10/App/Views/Admin/bots.php
T
li 4a94fe36cf 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 命名空间
2026-03-27 18:41:06 +08:00

445 lines
58 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>