- 拆分 HomeController → TransactionController, ReportWebController, FollowPlanController - 新增 Service 层: TransactionService, ReportService, FollowPlanService - pk10.php JS 抽离为 4 个独立文件 (sound/race/bet/poll) - 前台新增报表查询页面 (/report) + 跟单计划页面 (/follow-plan) - 后台新增跟单计划管理 + 用户输赢明细统计 - 封盘状态显示倒计时 (x:xx) - 音效仅在开奖弹窗打开时播放 - 路由按模块分组整理 - autoload 支持 App\Services 命名空间
445 lines
58 KiB
PHP
445 lines
58 KiB
PHP
<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>
|