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

- 拆分 HomeController → TransactionController, ReportWebController, FollowPlanController
- 新增 Service 层: TransactionService, ReportService, FollowPlanService
- pk10.php JS 抽离为 4 个独立文件 (sound/race/bet/poll)
- 前台新增报表查询页面 (/report) + 跟单计划页面 (/follow-plan)
- 后台新增跟单计划管理 + 用户输赢明细统计
- 封盘状态显示倒计时 (x:xx)
- 音效仅在开奖弹窗打开时播放
- 路由按模块分组整理
- autoload 支持 App\Services 命名空间
This commit is contained in:
li
2026-03-27 18:41:06 +08:00
parent 48cfe4be04
commit 4a94fe36cf
87 changed files with 8681 additions and 646 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ class AgentController extends AdminBaseController {
// 创建代理:先创建用户,再创建代理记录
$userId = (int)($data['user_id'] ?? 0);
if (!$userId) { $this->json(['status'=>'error','message'=>'User ID required']); return; }
$code = strtoupper(substr(md5(uniqid()), 0, 8));
$code = strtoupper(bin2hex(random_bytes(4)));
$this->db->insert('agents', [
'user_id' => $userId,
'parent_id' => !empty($data['parent_id']) ? (int)$data['parent_id'] : null,
+965
View File
@@ -0,0 +1,965 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
class BotController extends AdminBaseController
{
private Database $db;
public function __construct()
{
$this->checkLogin();
$this->checkAdmin();
$this->db = new Database();
}
public function index(): void
{
$stats = [
'bot_count' => 0,
'group_count' => 0,
'wallet_count' => 0,
'enabled_group_count' => 0,
'member_count' => 0,
'shill_count' => 0,
'push_count' => 0,
'push_success_count' => 0,
'countdown_group_count' => 0,
'animation_group_count' => 0,
'api_request_count' => 0,
'api_signature_ok_count' => 0,
];
$bots = [];
$groups = [];
$wallets = [];
$rules = [];
$games = [];
$users = [];
$members = [];
$shills = [];
$recentOrders = [];
$pushLogs = [];
$apiRequestLogs = [];
$pushSummary = [
'countdown' => ['total' => 0, 'success' => 0, 'failed' => 0, 'pending' => 0, 'skipped' => 0],
'bet_success' => ['total' => 0, 'success' => 0, 'failed' => 0, 'pending' => 0, 'skipped' => 0],
'draw' => ['total' => 0, 'success' => 0, 'failed' => 0, 'pending' => 0, 'skipped' => 0],
'credit' => ['total' => 0, 'success' => 0, 'failed' => 0, 'pending' => 0, 'skipped' => 0],
'debit' => ['total' => 0, 'success' => 0, 'failed' => 0, 'pending' => 0, 'skipped' => 0],
'system' => ['total' => 0, 'success' => 0, 'failed' => 0, 'pending' => 0, 'skipped' => 0],
];
try {
$stats['bot_count'] = (int)($this->db->count('bot_instances') ?: 0);
$stats['group_count'] = (int)($this->db->count('bot_groups') ?: 0);
$stats['wallet_count'] = (int)($this->db->count('bot_group_wallets', ['status' => 1]) ?: 0);
$stats['enabled_group_count'] = (int)($this->db->count('bot_groups', ['status' => 1, 'bet_enabled' => 1]) ?: 0);
$stats['member_count'] = (int)($this->db->count('bot_group_members') ?: 0);
$stats['shill_count'] = (int)($this->db->count('bot_shills', ['enabled' => 1]) ?: 0);
$stats['push_count'] = (int)($this->db->count('bot_push_logs') ?: 0);
$stats['push_success_count'] = (int)($this->db->count('bot_push_logs', ['status' => 'success']) ?: 0);
$stats['countdown_group_count'] = (int)($this->db->count('bot_groups', ['remind_close_countdown' => 1]) ?: 0);
$stats['animation_group_count'] = (int)($this->db->count('bot_groups', ['animation_enabled' => 1]) ?: 0);
$stats['api_request_count'] = (int)($this->db->count('bot_api_request_logs') ?: 0);
$stats['api_signature_ok_count'] = (int)($this->db->count('bot_api_request_logs', ['signature_ok' => 1]) ?: 0);
$bots = $this->db->select('bot_instances', '*', ['ORDER' => ['id' => 'DESC']]) ?: [];
foreach ($bots as &$bot) {
$botId = (int)$bot['id'];
$bot['group_count'] = (int)($this->db->count('bot_groups', ['bot_id' => $botId]) ?: 0);
$bot['active_group_count'] = (int)($this->db->count('bot_groups', [
'bot_id' => $botId,
'status' => 1,
'bet_enabled' => 1,
]) ?: 0);
}
unset($bot);
$groups = $this->db->select('bot_groups', [
'[>]bot_instances' => ['bot_id' => 'id'],
'[>]games' => ['game_id' => 'id'],
], [
'bot_groups.id',
'bot_groups.bot_id',
'bot_groups.game_id',
'bot_groups.bet_format_rule_id',
'bot_groups.tg_group_id',
'bot_groups.group_name',
'bot_groups.group_type',
'bot_groups.status',
'bot_groups.bet_enabled',
'bot_groups.remind_bet_success',
'bot_groups.remind_draw_result',
'bot_groups.remind_close_countdown',
'bot_groups.animation_enabled',
'bot_groups.countdown_config',
'bot_groups.updated_at',
'bot_instances.name(bot_name)',
'games.name(game_name)',
], [
'ORDER' => ['bot_groups.id' => 'DESC'],
]) ?: [];
$wallets = $this->db->select('bot_group_wallets', [
'[>]bot_groups' => ['group_id' => 'id'],
'[>]users' => ['platform_user_id' => 'id'],
], [
'bot_group_wallets.id',
'bot_group_wallets.group_id',
'bot_group_wallets.platform_user_id',
'bot_group_wallets.wallet_mode',
'bot_group_wallets.status',
'bot_group_wallets.updated_at',
'bot_groups.group_name',
'bot_groups.tg_group_id',
'users.id(platform_user_id)',
'users.username(platform_username)',
'users.balance(platform_balance)',
], [
'ORDER' => ['bot_group_wallets.id' => 'DESC'],
]) ?: [];
$rules = $this->db->select('bot_bet_format_rules', ['id', 'rule_code', 'name', 'status'], [
'status' => 1,
'ORDER' => ['id' => 'DESC'],
]) ?: [];
$games = $this->db->select('games', ['id', 'name', 'type', 'status'], [
'status' => 1,
'ORDER' => ['sort_order' => 'ASC', 'id' => 'DESC'],
]) ?: [];
$users = $this->db->select('users', ['id', 'username', 'balance', 'status', 'role'], [
'role[!]' => 'admin',
'ORDER' => ['id' => 'DESC'],
'LIMIT' => 200,
]) ?: [];
$members = $this->db->select('bot_group_members', [
'[>]bot_groups' => ['group_id' => 'id'],
'[>]users' => ['platform_user_id' => 'id'],
], [
'bot_group_members.id',
'bot_group_members.group_id',
'bot_group_members.tg_user_id',
'bot_group_members.tg_username',
'bot_group_members.tg_nickname',
'bot_group_members.platform_user_id',
'bot_group_members.role',
'bot_group_members.bet_enabled',
'bot_group_members.last_seen_at',
'bot_group_members.updated_at',
'bot_groups.group_name',
'bot_groups.tg_group_id',
'users.username(platform_username)',
], [
'ORDER' => ['bot_group_members.id' => 'DESC'],
'LIMIT' => 200,
]) ?: [];
$shills = $this->db->select('bot_shills', [
'[>]bot_groups' => ['group_id' => 'id'],
'[>]bot_group_members' => [
'bot_shills.group_id' => 'group_id',
'bot_shills.tg_user_id' => 'tg_user_id',
],
], [
'bot_shills.id',
'bot_shills.group_id',
'bot_shills.tg_user_id',
'bot_shills.note',
'bot_shills.enabled',
'bot_shills.updated_at',
'bot_groups.group_name',
'bot_groups.tg_group_id',
'bot_group_members.tg_username',
'bot_group_members.tg_nickname',
], [
'ORDER' => ['bot_shills.id' => 'DESC'],
'LIMIT' => 200,
]) ?: [];
$recentOrders = $this->db->select('bot_bet_orders', [
'[>]bot_groups' => ['group_id' => 'id'],
], [
'bot_bet_orders.id(order_id)',
'bot_bet_orders.period_number',
'bot_bet_orders.bet_amount_total',
'bot_bet_orders.accepted_bet_count',
'bot_bet_orders.sync_status',
'bot_bet_orders.sync_error',
'bot_bet_orders.is_shill',
'bot_bet_orders.created_at',
'bot_bet_orders.tg_username',
'bot_groups.group_name',
'bot_groups.tg_group_id',
], [
'ORDER' => ['bot_bet_orders.id' => 'DESC'],
'LIMIT' => 10,
]) ?: [];
$pushLogs = $this->db->select('bot_push_logs', [
'[>]bot_groups' => ['group_id' => 'id'],
], [
'bot_push_logs.id',
'bot_push_logs.group_id',
'bot_push_logs.period_number',
'bot_push_logs.push_type',
'bot_push_logs.payload_json',
'bot_push_logs.tg_message_id',
'bot_push_logs.status',
'bot_push_logs.error_message',
'bot_push_logs.created_at',
'bot_groups.group_name',
'bot_groups.tg_group_id',
], [
'ORDER' => ['bot_push_logs.id' => 'DESC'],
'LIMIT' => 20,
]) ?: [];
foreach ($pushLogs as &$pushLog) {
$payload = [];
if (!empty($pushLog['payload_json'])) {
$decodedPayload = json_decode((string)$pushLog['payload_json'], true);
if (is_array($decodedPayload)) {
$payload = $decodedPayload;
}
}
$pushLog['payload'] = $payload;
$pushType = (string)($pushLog['push_type'] ?? 'system');
$status = (string)($pushLog['status'] ?? 'pending');
if (!isset($pushSummary[$pushType])) {
$pushSummary[$pushType] = ['total' => 0, 'success' => 0, 'failed' => 0, 'pending' => 0, 'skipped' => 0];
}
$pushSummary[$pushType]['total']++;
if (isset($pushSummary[$pushType][$status])) {
$pushSummary[$pushType][$status]++;
}
}
unset($pushLog);
$apiRequestLogs = $this->db->select('bot_api_request_logs', [
'[>]bot_instances' => ['bot_id' => 'id'],
'[>]bot_groups' => ['group_id' => 'id'],
], [
'bot_api_request_logs.id',
'bot_api_request_logs.request_uri',
'bot_api_request_logs.http_method',
'bot_api_request_logs.idempotency_key',
'bot_api_request_logs.response_code',
'bot_api_request_logs.client_ip',
'bot_api_request_logs.signature_ok',
'bot_api_request_logs.created_at',
'bot_instances.name(bot_name)',
'bot_groups.group_name',
'bot_groups.tg_group_id',
], [
'ORDER' => ['bot_api_request_logs.id' => 'DESC'],
'LIMIT' => 20,
]) ?: [];
} catch (\Throwable $e) {
}
$this->render('Admin/bots.php', compact(
'stats', 'bots', 'groups', 'wallets', 'rules', 'games', 'users', 'members', 'shills', 'recentOrders', 'pushLogs', 'pushSummary', 'apiRequestLogs'
) + ['title' => 'Bot 管理']);
}
public function saveBot(): void
{
$payload = $this->getRequestData();
$name = trim((string)($payload['name'] ?? ''));
$botToken = trim((string)($payload['bot_token'] ?? ''));
$botUsername = $this->normalizeNullableString($payload['bot_username'] ?? null);
$runMode = $this->normalizeRunMode((string)($payload['run_mode'] ?? 'polling'));
$webhookUrl = $this->normalizeNullableString($payload['webhook_url'] ?? null);
$remark = $this->normalizeNullableString($payload['remark'] ?? null);
if ($name === '' || $botToken === '') {
$this->json(['success' => false, 'message' => 'Bot 名称和 Token 必填']);
return;
}
try {
$id = $this->db->insert('bot_instances', [
'name' => $name,
'bot_token' => $botToken,
'bot_username' => $botUsername,
'bot_key' => bin2hex(random_bytes(8)),
'bot_secret' => bin2hex(random_bytes(16)),
'webhook_url' => $webhookUrl,
'run_mode' => $runMode,
'status' => 1,
'remark' => $remark,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
$this->json(['success' => true, 'message' => 'Bot 实例创建成功', 'data' => ['id' => (int)$id]]);
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => 'Bot 实例创建失败:' . $e->getMessage()]);
}
}
public function updateBot(): void
{
$payload = $this->getRequestData();
$id = (int)($payload['id'] ?? 0);
$name = trim((string)($payload['name'] ?? ''));
$botToken = trim((string)($payload['bot_token'] ?? ''));
$botUsername = $this->normalizeNullableString($payload['bot_username'] ?? null);
$runMode = $this->normalizeRunMode((string)($payload['run_mode'] ?? 'polling'));
$webhookUrl = $this->normalizeNullableString($payload['webhook_url'] ?? null);
$remark = $this->normalizeNullableString($payload['remark'] ?? null);
if ($id <= 0 || $name === '' || $botToken === '') {
$this->json(['success' => false, 'message' => 'Bot ID、名称和 Token 必填']);
return;
}
try {
$bot = $this->db->get('bot_instances', ['id'], ['id' => $id]);
if (!$bot) {
$this->json(['success' => false, 'message' => 'Bot 实例不存在']);
return;
}
$this->db->update('bot_instances', [
'name' => $name,
'bot_token' => $botToken,
'bot_username' => $botUsername,
'webhook_url' => $webhookUrl,
'run_mode' => $runMode,
'remark' => $remark,
'updated_at' => date('Y-m-d H:i:s'),
], ['id' => $id]);
$this->json(['success' => true, 'message' => 'Bot 实例更新成功', 'data' => ['id' => $id]]);
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => 'Bot 实例更新失败:' . $e->getMessage()]);
}
}
public function toggleBot(): void
{
$this->toggleRecord('bot_instances', 'Bot 参数无效', 'Bot 实例不存在', 'Bot %s已%s');
}
public function saveGroup(): void
{
$payload = $this->getRequestData();
$botId = (int)($payload['bot_id'] ?? 0);
$gameId = (int)($payload['game_id'] ?? 0);
$tgGroupId = trim((string)($payload['tg_group_id'] ?? ''));
$groupName = trim((string)($payload['group_name'] ?? ''));
$groupType = $this->normalizeGroupType((string)($payload['group_type'] ?? 'group'));
$ruleId = (int)($payload['bet_format_rule_id'] ?? 0);
if ($botId <= 0 || $gameId <= 0 || $tgGroupId === '' || $groupName === '') {
$this->json(['success' => false, 'message' => 'Bot、游戏、群ID、群名称均必填']);
return;
}
try {
if (!$this->exists('bot_instances', $botId) || !$this->exists('games', $gameId)) {
$this->json(['success' => false, 'message' => 'Bot 或游戏不存在']);
return;
}
$id = $this->db->insert('bot_groups', [
'bot_id' => $botId,
'tg_group_id' => $tgGroupId,
'group_name' => $groupName,
'group_type' => $groupType,
'game_id' => $gameId,
'bet_format_rule_id' => $ruleId > 0 ? $ruleId : null,
'remind_bet_success' => !empty($payload['remind_bet_success']) ? 1 : 0,
'remind_draw_result' => !empty($payload['remind_draw_result']) ? 1 : 0,
'remind_close_countdown' => !empty($payload['remind_close_countdown']) ? 1 : 0,
'countdown_config' => !empty($payload['countdown_config']) ? $this->normalizeCountdownConfig((string)$payload['countdown_config']) : null,
'animation_enabled' => !empty($payload['animation_enabled']) ? 1 : 0,
'bet_enabled' => !empty($payload['bet_enabled']) ? 1 : 0,
'status' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
$this->json(['success' => true, 'message' => '群配置创建成功', 'data' => ['id' => (int)$id]]);
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => '群配置创建失败:' . $e->getMessage()]);
}
}
public function updateGroup(): void
{
$payload = $this->getRequestData();
$id = (int)($payload['id'] ?? 0);
$botId = (int)($payload['bot_id'] ?? 0);
$gameId = (int)($payload['game_id'] ?? 0);
$tgGroupId = trim((string)($payload['tg_group_id'] ?? ''));
$groupName = trim((string)($payload['group_name'] ?? ''));
$groupType = $this->normalizeGroupType((string)($payload['group_type'] ?? 'group'));
$ruleId = (int)($payload['bet_format_rule_id'] ?? 0);
if ($id <= 0 || $botId <= 0 || $gameId <= 0 || $tgGroupId === '' || $groupName === '') {
$this->json(['success' => false, 'message' => '群配置参数不完整']);
return;
}
try {
if (!$this->exists('bot_groups', $id)) {
$this->json(['success' => false, 'message' => '群配置不存在']);
return;
}
if (!$this->exists('bot_instances', $botId) || !$this->exists('games', $gameId)) {
$this->json(['success' => false, 'message' => 'Bot 或游戏不存在']);
return;
}
$this->db->update('bot_groups', [
'bot_id' => $botId,
'tg_group_id' => $tgGroupId,
'group_name' => $groupName,
'group_type' => $groupType,
'game_id' => $gameId,
'bet_format_rule_id' => $ruleId > 0 ? $ruleId : null,
'remind_bet_success' => !empty($payload['remind_bet_success']) ? 1 : 0,
'remind_draw_result' => !empty($payload['remind_draw_result']) ? 1 : 0,
'remind_close_countdown' => !empty($payload['remind_close_countdown']) ? 1 : 0,
'countdown_config' => !empty($payload['countdown_config']) ? $this->normalizeCountdownConfig((string)$payload['countdown_config']) : null,
'animation_enabled' => !empty($payload['animation_enabled']) ? 1 : 0,
'bet_enabled' => !empty($payload['bet_enabled']) ? 1 : 0,
'updated_at' => date('Y-m-d H:i:s'),
], ['id' => $id]);
$this->json(['success' => true, 'message' => '群配置更新成功', 'data' => ['id' => $id]]);
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => '群配置更新失败:' . $e->getMessage()]);
}
}
public function toggleGroup(): void
{
$this->toggleRecord('bot_groups', '群配置参数无效', '群配置不存在', '群 %s已%s', 'group_name');
}
public function saveWallet(): void
{
$payload = $this->getRequestData();
$groupId = (int)($payload['group_id'] ?? 0);
$platformUserId = (int)($payload['platform_user_id'] ?? 0);
$walletMode = $this->normalizeWalletMode((string)($payload['wallet_mode'] ?? 'master_pool'));
if ($groupId <= 0 || $platformUserId <= 0) {
$this->json(['success' => false, 'message' => '群配置和总账号必填']);
return;
}
try {
$user = $this->db->get('users', ['id', 'username'], ['id' => $platformUserId]);
if (!$this->exists('bot_groups', $groupId) || !$user) {
$this->json(['success' => false, 'message' => '群配置或网站账号不存在']);
return;
}
$exists = $this->db->get('bot_group_wallets', ['id'], ['group_id' => $groupId]);
$data = [
'platform_user_id' => $platformUserId,
'platform_username_snapshot' => $user['username'],
'wallet_mode' => $walletMode,
'status' => 1,
'updated_at' => date('Y-m-d H:i:s'),
];
if ($exists) {
$this->db->update('bot_group_wallets', $data, ['group_id' => $groupId]);
$id = (int)$exists['id'];
$message = '群总账号绑定已更新';
} else {
$data['group_id'] = $groupId;
$data['created_at'] = date('Y-m-d H:i:s');
$id = (int)$this->db->insert('bot_group_wallets', $data);
$message = '群总账号绑定成功';
}
$this->json(['success' => true, 'message' => $message, 'data' => ['id' => $id]]);
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => '群总账号绑定失败:' . $e->getMessage()]);
}
}
public function updateWallet(): void
{
$payload = $this->getRequestData();
$id = (int)($payload['id'] ?? 0);
$groupId = (int)($payload['group_id'] ?? 0);
$platformUserId = (int)($payload['platform_user_id'] ?? 0);
$walletMode = $this->normalizeWalletMode((string)($payload['wallet_mode'] ?? 'master_pool'));
if ($id <= 0 || $groupId <= 0 || $platformUserId <= 0) {
$this->json(['success' => false, 'message' => '钱包绑定参数不完整']);
return;
}
try {
$wallet = $this->db->get('bot_group_wallets', ['id'], ['id' => $id]);
$user = $this->db->get('users', ['id', 'username'], ['id' => $platformUserId]);
if (!$wallet) {
$this->json(['success' => false, 'message' => '钱包绑定不存在']);
return;
}
if (!$this->exists('bot_groups', $groupId) || !$user) {
$this->json(['success' => false, 'message' => '群配置或网站账号不存在']);
return;
}
$duplicate = $this->db->get('bot_group_wallets', ['id'], [
'group_id' => $groupId,
'id[!]' => $id,
]);
if ($duplicate) {
$this->json(['success' => false, 'message' => '该群已绑定其他总账号']);
return;
}
$this->db->update('bot_group_wallets', [
'group_id' => $groupId,
'platform_user_id' => $platformUserId,
'platform_username_snapshot' => $user['username'],
'wallet_mode' => $walletMode,
'updated_at' => date('Y-m-d H:i:s'),
], ['id' => $id]);
$this->json(['success' => true, 'message' => '群总账号绑定更新成功', 'data' => ['id' => $id]]);
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => '群总账号绑定更新失败:' . $e->getMessage()]);
}
}
public function toggleWallet(): void
{
$this->toggleRecord('bot_group_wallets', '钱包绑定参数无效', '钱包绑定不存在', '群总账号绑定已%s', 'id', false);
}
public function saveMember(): void
{
$payload = $this->getRequestData();
$groupId = (int)($payload['group_id'] ?? 0);
$tgUserId = trim((string)($payload['tg_user_id'] ?? ''));
$tgUsername = $this->normalizeNullableString($payload['tg_username'] ?? null);
$tgNickname = $this->normalizeNullableString($payload['tg_nickname'] ?? null);
$platformUserId = (int)($payload['platform_user_id'] ?? 0);
$role = $this->normalizeMemberRole((string)($payload['role'] ?? 'member'));
$betEnabled = !empty($payload['bet_enabled']) ? 1 : 0;
if ($groupId <= 0 || $tgUserId === '') {
$this->json(['success' => false, 'message' => '群配置与 TG 用户 ID 必填']);
return;
}
try {
if (!$this->exists('bot_groups', $groupId)) {
$this->json(['success' => false, 'message' => '群配置不存在']);
return;
}
if ($platformUserId > 0 && !$this->exists('users', $platformUserId)) {
$this->json(['success' => false, 'message' => '绑定网站账号不存在']);
return;
}
$id = (int)$this->db->insert('bot_group_members', [
'group_id' => $groupId,
'tg_user_id' => $tgUserId,
'tg_username' => $tgUsername,
'tg_nickname' => $tgNickname,
'platform_user_id' => $platformUserId > 0 ? $platformUserId : null,
'role' => $role,
'bet_enabled' => $betEnabled,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
$this->syncShillRecord($groupId, $tgUserId, $role === 'shill', $this->normalizeNullableString($payload['shill_note'] ?? null));
$this->json(['success' => true, 'message' => '群成员映射创建成功', 'data' => ['id' => $id]]);
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => '群成员映射创建失败:' . $e->getMessage()]);
}
}
public function updateMember(): void
{
$payload = $this->getRequestData();
$id = (int)($payload['id'] ?? 0);
$groupId = (int)($payload['group_id'] ?? 0);
$tgUserId = trim((string)($payload['tg_user_id'] ?? ''));
$tgUsername = $this->normalizeNullableString($payload['tg_username'] ?? null);
$tgNickname = $this->normalizeNullableString($payload['tg_nickname'] ?? null);
$platformUserId = (int)($payload['platform_user_id'] ?? 0);
$role = $this->normalizeMemberRole((string)($payload['role'] ?? 'member'));
$betEnabled = !empty($payload['bet_enabled']) ? 1 : 0;
if ($id <= 0 || $groupId <= 0 || $tgUserId === '') {
$this->json(['success' => false, 'message' => '成员参数不完整']);
return;
}
try {
if (!$this->exists('bot_group_members', $id)) {
$this->json(['success' => false, 'message' => '群成员映射不存在']);
return;
}
if (!$this->exists('bot_groups', $groupId)) {
$this->json(['success' => false, 'message' => '群配置不存在']);
return;
}
if ($platformUserId > 0 && !$this->exists('users', $platformUserId)) {
$this->json(['success' => false, 'message' => '绑定网站账号不存在']);
return;
}
$duplicate = $this->db->get('bot_group_members', ['id'], [
'group_id' => $groupId,
'tg_user_id' => $tgUserId,
'id[!]' => $id,
]);
if ($duplicate) {
$this->json(['success' => false, 'message' => '该 TG 用户已存在于当前群']);
return;
}
$this->db->update('bot_group_members', [
'group_id' => $groupId,
'tg_user_id' => $tgUserId,
'tg_username' => $tgUsername,
'tg_nickname' => $tgNickname,
'platform_user_id' => $platformUserId > 0 ? $platformUserId : null,
'role' => $role,
'bet_enabled' => $betEnabled,
'updated_at' => date('Y-m-d H:i:s'),
], ['id' => $id]);
$this->syncShillRecord($groupId, $tgUserId, $role === 'shill', $this->normalizeNullableString($payload['shill_note'] ?? null));
$this->json(['success' => true, 'message' => '群成员映射更新成功', 'data' => ['id' => $id]]);
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => '群成员映射更新失败:' . $e->getMessage()]);
}
}
public function toggleMemberBet(): void
{
$payload = $this->getRequestData();
$id = (int)($payload['id'] ?? 0);
$betEnabled = isset($payload['bet_enabled']) ? (int)$payload['bet_enabled'] : null;
if ($id <= 0 || !in_array($betEnabled, [0, 1], true)) {
$this->json(['success' => false, 'message' => '成员参数无效']);
return;
}
try {
$member = $this->db->get('bot_group_members', ['id', 'tg_user_id'], ['id' => $id]);
if (!$member) {
$this->json(['success' => false, 'message' => '群成员映射不存在']);
return;
}
$this->db->update('bot_group_members', [
'bet_enabled' => $betEnabled,
'updated_at' => date('Y-m-d H:i:s'),
], ['id' => $id]);
$this->json([
'success' => true,
'message' => sprintf('成员 %s 下注权限已%s', (string)$member['tg_user_id'], $betEnabled === 1 ? '开启' : '关闭'),
'data' => ['id' => $id, 'bet_enabled' => $betEnabled],
]);
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => '成员下注权限更新失败:' . $e->getMessage()]);
}
}
public function saveShill(): void
{
$payload = $this->getRequestData();
$groupId = (int)($payload['group_id'] ?? 0);
$tgUserId = trim((string)($payload['tg_user_id'] ?? ''));
$note = $this->normalizeNullableString($payload['note'] ?? null);
if ($groupId <= 0 || $tgUserId === '') {
$this->json(['success' => false, 'message' => '群配置与 TG 用户 ID 必填']);
return;
}
try {
if (!$this->exists('bot_groups', $groupId)) {
$this->json(['success' => false, 'message' => '群配置不存在']);
return;
}
$member = $this->db->get('bot_group_members', ['id'], [
'group_id' => $groupId,
'tg_user_id' => $tgUserId,
]);
if ($member) {
$this->db->update('bot_group_members', [
'role' => 'shill',
'updated_at' => date('Y-m-d H:i:s'),
], ['id' => (int)$member['id']]);
}
$existing = $this->db->get('bot_shills', ['id'], [
'group_id' => $groupId,
'tg_user_id' => $tgUserId,
]);
if ($existing) {
$this->db->update('bot_shills', [
'note' => $note,
'enabled' => 1,
'updated_at' => date('Y-m-d H:i:s'),
], ['id' => (int)$existing['id']]);
$id = (int)$existing['id'];
$message = '托号已更新';
} else {
$id = (int)$this->db->insert('bot_shills', [
'group_id' => $groupId,
'tg_user_id' => $tgUserId,
'note' => $note,
'enabled' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
$message = '托号创建成功';
}
$this->json(['success' => true, 'message' => $message, 'data' => ['id' => $id]]);
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => '托号创建失败:' . $e->getMessage()]);
}
}
public function updateShill(): void
{
$payload = $this->getRequestData();
$id = (int)($payload['id'] ?? 0);
$groupId = (int)($payload['group_id'] ?? 0);
$tgUserId = trim((string)($payload['tg_user_id'] ?? ''));
$note = $this->normalizeNullableString($payload['note'] ?? null);
if ($id <= 0 || $groupId <= 0 || $tgUserId === '') {
$this->json(['success' => false, 'message' => '托号参数不完整']);
return;
}
try {
$shill = $this->db->get('bot_shills', ['id'], ['id' => $id]);
if (!$shill) {
$this->json(['success' => false, 'message' => '托号记录不存在']);
return;
}
if (!$this->exists('bot_groups', $groupId)) {
$this->json(['success' => false, 'message' => '群配置不存在']);
return;
}
$duplicate = $this->db->get('bot_shills', ['id'], [
'group_id' => $groupId,
'tg_user_id' => $tgUserId,
'id[!]' => $id,
]);
if ($duplicate) {
$this->json(['success' => false, 'message' => '该托号已存在']);
return;
}
$this->db->update('bot_shills', [
'group_id' => $groupId,
'tg_user_id' => $tgUserId,
'note' => $note,
'updated_at' => date('Y-m-d H:i:s'),
], ['id' => $id]);
$member = $this->db->get('bot_group_members', ['id'], [
'group_id' => $groupId,
'tg_user_id' => $tgUserId,
]);
if ($member) {
$this->db->update('bot_group_members', [
'role' => 'shill',
'updated_at' => date('Y-m-d H:i:s'),
], ['id' => (int)$member['id']]);
}
$this->json(['success' => true, 'message' => '托号更新成功', 'data' => ['id' => $id]]);
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => '托号更新失败:' . $e->getMessage()]);
}
}
public function toggleShill(): void
{
$payload = $this->getRequestData();
$id = (int)($payload['id'] ?? 0);
$enabled = isset($payload['enabled']) ? (int)$payload['enabled'] : null;
if ($id <= 0 || !in_array($enabled, [0, 1], true)) {
$this->json(['success' => false, 'message' => '托号参数无效']);
return;
}
try {
$shill = $this->db->get('bot_shills', ['id', 'group_id', 'tg_user_id'], ['id' => $id]);
if (!$shill) {
$this->json(['success' => false, 'message' => '托号记录不存在']);
return;
}
$this->db->update('bot_shills', [
'enabled' => $enabled,
'updated_at' => date('Y-m-d H:i:s'),
], ['id' => $id]);
$member = $this->db->get('bot_group_members', ['id'], [
'group_id' => (int)$shill['group_id'],
'tg_user_id' => (string)$shill['tg_user_id'],
]);
if ($member) {
$this->db->update('bot_group_members', [
'role' => $enabled === 1 ? 'shill' : 'member',
'updated_at' => date('Y-m-d H:i:s'),
], ['id' => (int)$member['id']]);
}
$this->json([
'success' => true,
'message' => sprintf('托号 %s 已%s统计排除', (string)$shill['tg_user_id'], $enabled === 1 ? '加入' : '移出'),
'data' => ['id' => $id, 'enabled' => $enabled],
]);
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => '托号状态更新失败:' . $e->getMessage()]);
}
}
private function getRequestData(): array
{
$data = json_decode(file_get_contents('php://input'), true);
if (is_array($data) && !empty($data)) {
return $data;
}
return $_POST;
}
private function normalizeCountdownConfig(string $raw): string
{
$items = array_filter(array_map('trim', explode(',', $raw)), static function ($item) {
return $item !== '' && ctype_digit($item);
});
$numbers = array_map('intval', $items);
rsort($numbers);
return json_encode(array_values(array_unique($numbers)), JSON_UNESCAPED_UNICODE);
}
private function normalizeNullableString($value): ?string
{
$text = trim((string)$value);
return $text === '' ? null : $text;
}
private function normalizeRunMode(string $runMode): string
{
return in_array($runMode, ['polling', 'webhook'], true) ? $runMode : 'polling';
}
private function normalizeGroupType(string $groupType): string
{
return in_array($groupType, ['group', 'supergroup', 'channel'], true) ? $groupType : 'group';
}
private function normalizeWalletMode(string $walletMode): string
{
return in_array($walletMode, ['master_pool', 'per_member'], true) ? $walletMode : 'master_pool';
}
private function normalizeMemberRole(string $role): string
{
return in_array($role, ['member', 'admin', 'shill'], true) ? $role : 'member';
}
private function exists(string $table, int $id): bool
{
return $id > 0 && $this->db->has($table, ['id' => $id]);
}
private function syncShillRecord(int $groupId, string $tgUserId, bool $enabled, ?string $note = null): void
{
$existing = $this->db->get('bot_shills', ['id'], [
'group_id' => $groupId,
'tg_user_id' => $tgUserId,
]);
if ($enabled) {
$data = [
'note' => $note,
'enabled' => 1,
'updated_at' => date('Y-m-d H:i:s'),
];
if ($existing) {
$this->db->update('bot_shills', $data, ['id' => (int)$existing['id']]);
} else {
$data['group_id'] = $groupId;
$data['tg_user_id'] = $tgUserId;
$data['created_at'] = date('Y-m-d H:i:s');
$this->db->insert('bot_shills', $data);
}
return;
}
if ($existing) {
$this->db->update('bot_shills', [
'enabled' => 0,
'note' => $note,
'updated_at' => date('Y-m-d H:i:s'),
], ['id' => (int)$existing['id']]);
}
}
private function toggleRecord(string $table, string $invalidMessage, string $notFoundMessage, string $messageTemplate, string $nameField = 'name', bool $withName = true): void
{
$payload = $this->getRequestData();
$id = (int)($payload['id'] ?? 0);
$status = isset($payload['status']) ? (int)$payload['status'] : null;
if ($id <= 0 || !in_array($status, [0, 1], true)) {
$this->json(['success' => false, 'message' => $invalidMessage]);
return;
}
try {
$row = $this->db->get($table, ['id', $nameField], ['id' => $id]);
if (!$row) {
$this->json(['success' => false, 'message' => $notFoundMessage]);
return;
}
$this->db->update($table, [
'status' => $status,
'updated_at' => date('Y-m-d H:i:s'),
], ['id' => $id]);
$message = $withName
? sprintf($messageTemplate, (string)($row[$nameField] ?? $id), $status === 1 ? '启用' : '停用')
: sprintf($messageTemplate, $status === 1 ? '启用' : '停用');
$this->json(['success' => true, 'message' => $message, 'data' => ['id' => $id, 'status' => $status]]);
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => '状态更新失败:' . $e->getMessage()]);
}
}
private function json(array $data): void
{
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
}
@@ -0,0 +1,53 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use App\Services\FollowPlanService;
use Db\Database;
class FollowPlanController extends AdminBaseController {
private $db;
public function __construct(Database $db) {
$this->db = $db;
}
public function index() {
$this->checkLogin(); $this->checkAdmin();
$service = new FollowPlanService($this->db);
$plans = $service->getAllPlans();
$games = $this->db->select('games', ['id', 'name', 'code'], ['status' => 1]);
$this->render('Admin/follow_plans.php', compact('plans', 'games'));
}
public function save() {
$this->checkLogin(); $this->checkAdmin();
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$data['created_by'] = $_SESSION['admin_id'] ?? 0;
$service = new FollowPlanService($this->db);
echo json_encode($service->savePlan($data));
}
public function delete() {
$this->checkLogin(); $this->checkAdmin();
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$id = intval($data['id'] ?? 0);
if ($id <= 0) { echo json_encode(['success' => false, 'message' => 'ID invalid']); return; }
$service = new FollowPlanService($this->db);
echo json_encode($service->deletePlan($id));
}
public function toggle() {
$this->checkLogin(); $this->checkAdmin();
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$id = intval($data['id'] ?? 0);
$plan = $this->db->get('follow_plans', '*', ['id' => $id]);
if (!$plan) { echo json_encode(['success' => false, 'message' => 'Not found']); return; }
$newStatus = $plan['status'] ? 0 : 1;
$this->db->update('follow_plans', ['status' => $newStatus], ['id' => $id]);
echo json_encode(['success' => true, 'status' => $newStatus]);
}
}
@@ -0,0 +1,284 @@
<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use Db\Database;
class FundRequestController extends AdminBaseController {
public function __construct() {
$this->checkLogin();
}
/**
* 充提审核列表页
*/
public function index() {
$db = new Database();
try {
// 查询充提申请列表,JOIN users 获取用户名
$requests = $db->select('fund_requests', [
'[>]users' => ['user_id' => 'id']
], [
'fund_requests.id',
'fund_requests.user_id',
'users.username',
'users.usdt_address',
'fund_requests.type',
'fund_requests.amount',
'fund_requests.status',
'fund_requests.remark',
'fund_requests.admin_remark',
'fund_requests.operator_id',
'fund_requests.created_at',
'fund_requests.processed_at'
], [
'ORDER' => ['fund_requests.id' => 'DESC'],
'LIMIT' => 200
]);
if (!is_array($requests)) {
$requests = [];
}
// 统计各状态数量
$pendingCount = $db->count('fund_requests', ['status' => 'pending']);
$todayDeposit = $db->count('fund_requests', [
'type' => 'deposit',
'created_at[>=]' => date('Y-m-d 00:00:00')
]);
$todayWithdraw = $db->count('fund_requests', [
'type' => 'withdraw',
'created_at[>=]' => date('Y-m-d 00:00:00')
]);
} catch (\Throwable $e) {
$requests = [];
$pendingCount = 0;
$todayDeposit = 0;
$todayWithdraw = 0;
}
$this->render('Admin/fund_requests.php', [
'requests' => $requests,
'pendingCount' => $pendingCount,
'todayDeposit' => $todayDeposit,
'todayWithdraw' => $todayWithdraw,
'title' => '充提审核'
]);
}
/**
* 审批通过
*/
public function approve() {
header('Content-Type: application/json');
$rawData = file_get_contents('php://input');
$data = json_decode($rawData, true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo json_encode(['success' => false, 'message' => '数据格式错误']);
return;
}
$id = isset($data['id']) ? (int)$data['id'] : 0;
$adminRemark = trim($data['admin_remark'] ?? '');
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => '无效的申请ID']);
return;
}
$db = new Database();
try {
// 查询申请记录
$request = $db->get('fund_requests', '*', ['id' => $id]);
if (!$request) {
echo json_encode(['success' => false, 'message' => '申请记录不存在']);
return;
}
if ($request['status'] !== 'pending') {
echo json_encode(['success' => false, 'message' => '该申请已处理,状态:' . $request['status']]);
return;
}
$userId = (int)$request['user_id'];
$amount = floatval($request['amount']);
$type = $request['type'];
// 获取用户信息
$user = $db->get('users', ['id', 'balance'], ['id' => $userId]);
if (!$user) {
echo json_encode(['success' => false, 'message' => '用户不存在']);
return;
}
$balanceBefore = floatval($user['balance']);
$now = date('Y-m-d H:i:s');
$operatorId = $_SESSION['user_id'] ?? 0;
// 开启事务
$db->medoo->pdo->beginTransaction();
if ($type === 'deposit') {
// 充值:给用户加款
$balanceAfter = $balanceBefore + $amount;
$db->update('users', [
'balance' => $balanceAfter,
'updated_at' => $now
], ['id' => $userId]);
$db->insert('transactions', [
'user_id' => $userId,
'type' => 'deposit',
'amount' => $amount,
'balance_before' => $balanceBefore,
'balance_after' => $balanceAfter,
'description' => '充值申请审核通过 #' . $id,
'created_at' => $now
]);
} else {
// 提现:余额已在申请时预扣,确认放款
$balanceAfter = $balanceBefore; // 余额不变,已预扣
$db->insert('transactions', [
'user_id' => $userId,
'type' => 'withdraw',
'amount' => -$amount,
'balance_before' => $balanceBefore,
'balance_after' => $balanceAfter,
'description' => '提现申请审核通过 #' . $id,
'created_at' => $now
]);
}
// 更新申请状态
$db->update('fund_requests', [
'status' => 'approved',
'admin_remark' => $adminRemark,
'operator_id' => $operatorId,
'processed_at' => $now
], ['id' => $id]);
$db->medoo->pdo->commit();
echo json_encode(['success' => true, 'message' => '审批通过']);
} catch (\Exception $e) {
if ($db->medoo->pdo->inTransaction()) {
$db->medoo->pdo->rollBack();
}
echo json_encode(['success' => false, 'message' => '操作失败:' . $e->getMessage()]);
}
}
/**
* 拒绝申请
*/
public function reject() {
header('Content-Type: application/json');
$rawData = file_get_contents('php://input');
$data = json_decode($rawData, true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo json_encode(['success' => false, 'message' => '数据格式错误']);
return;
}
$id = isset($data['id']) ? (int)$data['id'] : 0;
$adminRemark = trim($data['admin_remark'] ?? '');
if ($id <= 0) {
echo json_encode(['success' => false, 'message' => '无效的申请ID']);
return;
}
$db = new Database();
try {
// 查询申请记录
$request = $db->get('fund_requests', '*', ['id' => $id]);
if (!$request) {
echo json_encode(['success' => false, 'message' => '申请记录不存在']);
return;
}
if ($request['status'] !== 'pending') {
echo json_encode(['success' => false, 'message' => '该申请已处理,状态:' . $request['status']]);
return;
}
$userId = (int)$request['user_id'];
$amount = floatval($request['amount']);
$type = $request['type'];
$now = date('Y-m-d H:i:s');
$operatorId = $_SESSION['user_id'] ?? 0;
// 开启事务
$db->medoo->pdo->beginTransaction();
if ($type === 'withdraw') {
// 提现拒绝:退还预扣余额
$user = $db->get('users', ['id', 'balance'], ['id' => $userId]);
if ($user) {
$balanceBefore = floatval($user['balance']);
$balanceAfter = $balanceBefore + $amount;
$db->update('users', [
'balance' => $balanceAfter,
'updated_at' => $now
], ['id' => $userId]);
$db->insert('transactions', [
'user_id' => $userId,
'type' => 'refund',
'amount' => $amount,
'balance_before' => $balanceBefore,
'balance_after' => $balanceAfter,
'description' => '提现申请被拒绝,退还预扣金额 #' . $id,
'created_at' => $now
]);
}
}
// 充值拒绝:无需退款
// 更新申请状态
$db->update('fund_requests', [
'status' => 'rejected',
'admin_remark' => $adminRemark,
'operator_id' => $operatorId,
'processed_at' => $now
], ['id' => $id]);
$db->medoo->pdo->commit();
echo json_encode(['success' => true, 'message' => '已拒绝']);
} catch (\Exception $e) {
if ($db->medoo->pdo->inTransaction()) {
$db->medoo->pdo->rollBack();
}
echo json_encode(['success' => false, 'message' => '操作失败:' . $e->getMessage()]);
}
}
/**
* 获取待处理数量(轮询接口)
*/
public function pendingCount() {
header('Content-Type: application/json');
$db = new Database();
try {
$count = $db->count('fund_requests', ['status' => 'pending']);
echo json_encode(['count' => (int)$count]);
} catch (\Throwable $e) {
echo json_encode(['count' => 0]);
}
}
}
+134 -2
View File
@@ -61,12 +61,23 @@ class PK10PeriodController extends AdminBaseController {
}
$periodNumber = PK10Algorithm::generatePeriodNumber($gameId);
$createdAt = date('Y-m-d H:i:s');
$this->db->insert('periods', [
'game_id' => $gameId,
'period_number' => $periodNumber,
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
'start_time' => $createdAt,
'created_at' => $createdAt,
]);
$periodId = (int)$this->db->id();
if ($periodId > 0) {
$this->queueCountdownPushes($gameId, [
'id' => $periodId,
'period_number' => $periodNumber,
'created_at' => $createdAt,
]);
}
$this->json(['status' => 'success', 'message' => 'New period started', 'period_number' => $periodNumber]);
}
@@ -83,6 +94,14 @@ class PK10PeriodController extends AdminBaseController {
}
$this->db->update('periods', ['status' => 'locked'], ['id' => $periodId]);
$this->queueBotPushByGame($period['game_id'], 'countdown', [
'event' => 'period_locked',
'period_id' => (int)$period['id'],
'period_number' => (string)($period['period_number'] ?? ''),
'message' => 'Period locked',
], [
'remind_close_countdown' => 1,
]);
$this->json(['status' => 'success', 'message' => 'Period locked']);
}
@@ -122,7 +141,8 @@ class PK10PeriodController extends AdminBaseController {
]);
if (!empty($waterConfig) && !empty($bets)) {
$result = PK10Algorithm::generateControlledResult($bets, $waterConfig);
$targetProfitRate = floatval(\App\Core\SettingsHelper::get('target_profit_rate'));
$result = PK10Algorithm::generateControlledResult($bets, $waterConfig, 100, $targetProfitRate);
} else {
$result = PK10Algorithm::generateResult();
}
@@ -150,6 +170,17 @@ class PK10PeriodController extends AdminBaseController {
'rank_10' => $result[9], 'champion_sum' => $sum,
]);
$this->queueBotPushByGame($period['game_id'], 'draw', [
'event' => 'period_drawn',
'period_id' => (int)$period['id'],
'period_number' => (string)($period['period_number'] ?? ''),
'result' => $result,
'champion_sum' => $sum,
'manual' => !empty($manual),
], [
'remind_draw_result' => 1,
], (string)($period['period_number'] ?? ''));
$this->db->medoo->pdo->commit();
} catch (\Throwable $e) {
$this->db->medoo->pdo->rollBack();
@@ -180,6 +211,15 @@ class PK10PeriodController extends AdminBaseController {
$bets = $this->db->select('bets', '*', ['period_id' => $periodId, 'status' => 'pending']);
if (empty($bets)) {
$this->db->update('periods', ['status' => 'settled'], ['id' => $periodId]);
$this->queueBotPushByGame($period['game_id'], 'system', [
'event' => 'period_settled',
'period_id' => (int)$period['id'],
'period_number' => (string)($period['period_number'] ?? ''),
'wins' => 0,
'losses' => 0,
'total_payout' => 0,
'message' => 'No bets to settle',
], [], (string)($period['period_number'] ?? ''));
$this->json(['status' => 'success', 'message' => 'No bets to settle', 'wins' => 0, 'losses' => 0]);
return;
}
@@ -230,6 +270,14 @@ class PK10PeriodController extends AdminBaseController {
$this->settleAgentCommissions($bets, $periodId);
$this->db->update('periods', ['status' => 'settled'], ['id' => $periodId]);
$this->queueBotPushByGame($period['game_id'], 'system', [
'event' => 'period_settled',
'period_id' => (int)$period['id'],
'period_number' => (string)($period['period_number'] ?? ''),
'wins' => $wins,
'losses' => $losses,
'total_payout' => (float)$totalPayout,
], [], (string)($period['period_number'] ?? ''));
$this->db->medoo->pdo->commit();
} catch (\Throwable $e) {
$this->db->medoo->pdo->rollBack();
@@ -298,6 +346,90 @@ class PK10PeriodController extends AdminBaseController {
return $game ? (int)$game : 0;
}
private function queueCountdownPushes(int $gameId, array $period): void {
if ($gameId <= 0 || empty($period['id']) || empty($period['period_number'])) {
return;
}
$groups = $this->db->select('bot_groups', ['id', 'countdown_config'], [
'game_id' => $gameId,
'status' => 1,
'remind_close_countdown' => 1,
]) ?: [];
if (empty($groups)) {
return;
}
$game = $this->db->get('games', ['period_duration', 'lock_before_end'], ['id' => $gameId]);
$periodDuration = (int)($game['period_duration'] ?? 300);
$lockBeforeEnd = max(0, (int)($game['lock_before_end'] ?? 30));
$periodCreatedAt = strtotime((string)($period['created_at'] ?? ''));
if ($periodCreatedAt <= 0) {
$periodCreatedAt = time();
}
$closeAtTs = $periodCreatedAt + max(0, $periodDuration - $lockBeforeEnd);
$now = date('Y-m-d H:i:s');
foreach ($groups as $group) {
$decoded = json_decode((string)($group['countdown_config'] ?? ''), true);
if (!is_array($decoded) || empty($decoded)) {
continue;
}
$secondsList = array_values(array_unique(array_filter(array_map('intval', $decoded), static function (int $seconds) use ($periodDuration, $lockBeforeEnd) {
return $seconds > 0 && $seconds <= max(0, $periodDuration - $lockBeforeEnd);
})));
rsort($secondsList);
foreach ($secondsList as $seconds) {
$dispatchAtTs = $closeAtTs - $seconds;
if ($dispatchAtTs <= $periodCreatedAt) {
continue;
}
$payload = [
'event' => 'countdown_tick',
'period_id' => (int)$period['id'],
'period_number' => (string)$period['period_number'],
'countdown_seconds' => $seconds,
'dispatch_at' => date('Y-m-d H:i:s', $dispatchAtTs),
'message' => sprintf('Bet closes in %d seconds', $seconds),
];
$this->db->insert('bot_push_logs', [
'group_id' => (int)$group['id'],
'period_number' => (string)$period['period_number'],
'push_type' => 'countdown',
'payload_json' => json_encode($payload, JSON_UNESCAPED_UNICODE),
'status' => 'pending',
'created_at' => $now,
'updated_at' => $now,
]);
}
}
}
private function queueBotPushByGame(int $gameId, string $pushType, array $payload, array $groupFilters = [], ?string $periodNumber = null): void {
if ($gameId <= 0) {
return;
}
$where = array_merge(['game_id' => $gameId, 'status' => 1], $groupFilters);
$groups = $this->db->select('bot_groups', ['id'], $where) ?: [];
$now = date('Y-m-d H:i:s');
foreach ($groups as $group) {
$this->db->insert('bot_push_logs', [
'group_id' => (int)$group['id'],
'period_number' => $periodNumber !== null ? $periodNumber : ((string)($payload['period_number'] ?? '') ?: null),
'push_type' => $pushType,
'payload_json' => json_encode($payload, JSON_UNESCAPED_UNICODE),
'status' => 'pending',
'created_at' => $now,
'updated_at' => $now,
]);
}
}
private function json(array $data) {
header('Content-Type: application/json');
echo json_encode($data);
+115 -77
View File
@@ -12,94 +12,132 @@ class ReportController extends AdminBaseController {
$this->checkLogin(); $this->checkAdmin();
$dateFrom = $_GET['from'] ?? date('Y-m-d', strtotime('-7 days'));
$dateTo = $_GET['to'] ?? date('Y-m-d');
$range = [$dateFrom . ' 00:00:00', $dateTo . ' 23:59:59'];
// 总投注(排除虚拟)
$totalBet = $this->db->sum('bets', 'amount', [
'is_virtual' => 0, 'created_at[<>]' => $range
]) ?: 0;
$reportService = new \App\Services\ReportService($this->db);
// 总派奖
$totalWin = $this->db->sum('bets', 'win_amount', [
'is_virtual' => 0, 'status' => 'win', 'created_at[<>]' => $range
]) ?: 0;
// 总体统计
$overview = $reportService->getOverviewStats($dateFrom, $dateTo);
extract($overview); // totalBet, totalWin, profit, profitRate, totalDeposit, totalWithdraw, totalCommission, totalRebate, totalUsers, newUsers, activeUsers
// 平台利润
$profit = $totalBet - $totalWin;
// 日明细统计
$dailyStats = $reportService->getDailyStats($dateFrom, $dateTo);
// 总充值
$totalDeposit = $this->db->sum('transactions', 'amount', [
'is_virtual' => 0, 'type[~]' => '%deposit%', 'amount[>]' => 0,
'created_at[<>]' => $range
]) ?: 0;
// 总提现
$totalWithdraw = abs($this->db->sum('transactions', 'amount', [
'is_virtual' => 0, 'type[~]' => '%withdraw%', 'amount[<]' => 0,
'created_at[<>]' => $range
]) ?: 0);
// 代理佣金
$totalCommission = $this->db->sum('agent_commissions', 'commission', [
'created_at[<>]' => $range
]) ?: 0;
// 反水
$totalRebate = $this->db->sum('agent_commissions', 'commission', [
'type' => 'rebate', 'created_at[<>]' => $range
]) ?: 0;
// 用户统计
$totalUsers = $this->db->count('users', ['is_virtual' => 0]);
$newUsers = $this->db->count('users', [
'is_virtual' => 0, 'created_at[<>]' => $range
]);
// 每日明细
$dailyStats = [];
$start = new \DateTime($dateFrom);
$end = new \DateTime($dateTo);
$end->modify('+1 day');
$interval = new \DateInterval('P1D');
$period = new \DatePeriod($start, $interval, $end);
foreach ($period as $day) {
$d = $day->format('Y-m-d');
$dr = [$d . ' 00:00:00', $d . ' 23:59:59'];
$dailyStats[] = [
'date' => $d,
'bets' => $this->db->sum('bets', 'amount', ['is_virtual'=>0,'created_at[<>]'=>$dr]) ?: 0,
'wins' => $this->db->sum('bets', 'win_amount', ['is_virtual'=>0,'status'=>'win','created_at[<>]'=>$dr]) ?: 0,
'deposits' => $this->db->sum('transactions', 'amount', ['is_virtual'=>0,'type[~]'=>'%deposit%','amount[>]'=>0,'created_at[<>]'=>$dr]) ?: 0,
'withdraws' => abs($this->db->sum('transactions', 'amount', ['is_virtual'=>0,'type[~]'=>'%withdraw%','amount[<]'=>0,'created_at[<>]'=>$dr]) ?: 0),
];
}
// 用户排行(投注前20
$topUsers = $reportService->getTopUsers($dateFrom, $dateTo, 20);
// 代理报表
$agentStats = [];
$agents = $this->db->select('agents', '*', ['status' => 1]);
foreach ($agents as $ag) {
$playerIds = $this->db->select('users', 'id', ['agent_id' => $ag['id'], 'is_virtual' => 0]);
$agBet = 0; $agWin = 0;
if (!empty($playerIds)) {
$agBet = $this->db->sum('bets', 'amount', ['user_id' => $playerIds, 'is_virtual'=>0, 'created_at[<>]'=>$range]) ?: 0;
$agWin = $this->db->sum('bets', 'win_amount', ['user_id' => $playerIds, 'is_virtual'=>0, 'status'=>'win', 'created_at[<>]'=>$range]) ?: 0;
}
$agComm = $this->db->sum('agent_commissions', 'commission', ['agent_id'=>$ag['id'],'created_at[<>]'=>$range]) ?: 0;
$agentStats[] = [
'agent' => $ag,
'user' => $this->db->get('users', ['username'], ['id' => $ag['user_id']]),
'players' => count($playerIds),
'bets' => $agBet, 'wins' => $agWin, 'commission' => $agComm,
'profit' => $agBet - $agWin - $agComm,
];
}
$agentStats = $reportService->getAgentStats($dateFrom, $dateTo);
// 用户输赢明细
$winLossData = $reportService->getUserWinLoss($dateFrom, $dateTo);
$userSummary = $winLossData['summary'];
$this->render('Admin/reports.php', compact(
'dateFrom','dateTo','totalBet','totalWin','profit',
'dateFrom','dateTo','totalBet','totalWin','profit','profitRate',
'totalDeposit','totalWithdraw','totalCommission','totalRebate',
'totalUsers','newUsers','dailyStats','agentStats'
'totalUsers','newUsers','activeUsers','dailyStats','topUsers','agentStats',
'userSummary'
));
}
/**
* CSV 导出
*/
public function export() {
$this->checkLogin(); $this->checkAdmin();
$dateFrom = $_GET['from'] ?? date('Y-m-d', strtotime('-7 days'));
$dateTo = $_GET['to'] ?? date('Y-m-d');
$reportService = new \App\Services\ReportService($this->db);
// 通过 Service 获取数据
$dailyStats = $reportService->getDailyStats($dateFrom, $dateTo);
$topUsers = $reportService->getTopUsers($dateFrom, $dateTo, 0); // 不限条数
$winLossData = $reportService->getUserWinLoss($dateFrom, $dateTo);
$userSummary = $winLossData['summary'];
$userWinLossRaw = $winLossData['raw'];
// 输出 CSV
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="report_' . $dateFrom . '_' . $dateTo . '.csv"');
// BOM 头(Excel 兼容中文)
echo "\xEF\xBB\xBF";
$out = fopen('php://output', 'w');
// 日明细
fputcsv($out, ['=== 日明细报表 ===']);
fputcsv($out, ['日期', '投注额', '派彩额', '利润', '投注笔数', '中奖笔数', '胜率']);
foreach ($dailyStats as $day) {
$totalBet = floatval($day['total_bet'] ?? 0);
$totalWin = floatval($day['total_win'] ?? 0);
$betCount = intval($day['bet_count'] ?? 0);
$winCount = intval($day['win_count'] ?? 0);
$winRate = $betCount > 0 ? round($winCount / $betCount * 100, 1) . '%' : '0%';
fputcsv($out, [
$day['date'],
number_format($totalBet, 2, '.', ''),
number_format($totalWin, 2, '.', ''),
number_format($totalBet - $totalWin, 2, '.', ''),
$betCount,
$winCount,
$winRate
]);
}
fputcsv($out, []);
fputcsv($out, ['=== 用户投注排行 ===']);
fputcsv($out, ['排名', '用户名', '投注额', '派彩额', '平台盈亏', '投注笔数']);
$rank = 1;
foreach ($topUsers as $user) {
fputcsv($out, [
$rank++,
$user['username'],
number_format(floatval($user['total_bet']), 2, '.', ''),
number_format(floatval($user['total_win']), 2, '.', ''),
number_format(floatval($user['profit']), 2, '.', ''),
intval($user['bet_count'])
]);
}
// 用户输赢明细
fputcsv($out, []);
fputcsv($out, ['=== 用户输赢明细 ===']);
fputcsv($out, ['用户名', '当前余额', '总投注', '总派彩', '平台盈亏(投注-派彩)', '投注笔数', '中奖笔数', '胜率']);
foreach ($userSummary as $ud) {
$tb = $ud['total_bet'];
$tw = $ud['total_win'];
$bc = $ud['bet_count'];
$wc = $ud['win_count'];
$wr = $bc > 0 ? round($wc / $bc * 100, 1) . '%' : '0%';
fputcsv($out, [
$ud['username'],
number_format($ud['balance'], 2, '.', ''),
number_format($tb, 2, '.', ''),
number_format($tw, 2, '.', ''),
number_format($tb - $tw, 2, '.', ''),
$bc, $wc, $wr
]);
}
// 用户日输赢明细
fputcsv($out, []);
fputcsv($out, ['=== 用户日输赢明细 ===']);
fputcsv($out, ['用户名', '日期', '投注额', '派彩额', '平台盈亏', '笔数']);
foreach ($userWinLossRaw as $dd) {
$tb = floatval($dd['total_bet']);
$tw = floatval($dd['total_win']);
fputcsv($out, [
$dd['username'], $dd['date'],
number_format($tb, 2, '.', ''),
number_format($tw, 2, '.', ''),
number_format($tb - $tw, 2, '.', ''),
intval($dd['bet_count'])
]);
}
fclose($out);
exit;
}
private function json($data) { header('Content-Type: application/json'); echo json_encode($data); }
}
@@ -107,6 +107,7 @@ class SettingsController extends AdminBaseController {
'site_logo', 'site_favicon', 'site_copyright',
'smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass',
'smtp_from', 'smtp_from_name', 'smtp_encryption',
'customer_service_url',
];
foreach ($allowedKeys as $key) {
+22 -1
View File
@@ -13,7 +13,8 @@ class WaterController extends AdminBaseController {
$gameId = $this->getGameId();
$configs = $this->db->select('water_control', '*', ['game_id' => $gameId]);
$limits = $this->db->select('bet_limits', '*', ['game_id' => $gameId]);
$this->render('Admin/water_control.php', compact('configs', 'limits', 'gameId'));
$targetProfitRate = \App\Core\SettingsHelper::get('target_profit_rate');
$this->render('Admin/water_control.php', compact('configs', 'limits', 'gameId', 'targetProfitRate'));
}
public function updateWater() {
@@ -61,6 +62,26 @@ class WaterController extends AdminBaseController {
$this->json(['status' => 'success']);
}
public function updateProfitRate() {
$this->checkLogin(); $this->checkAdmin();
$data = json_decode(file_get_contents('php://input'), true);
$rate = isset($data['target_profit_rate']) ? floatval($data['target_profit_rate']) : 15;
if ($rate < 0 || $rate > 100) {
$this->json(['status' => 'error', 'message' => '盈利率必须在0-100之间']);
return;
}
$db = $this->db;
$existing = $db->get('system_settings', 'id', ['setting_key' => 'target_profit_rate']);
if ($existing) {
$db->update('system_settings', ['setting_value' => (string)$rate], ['id' => $existing]);
} else {
$db->insert('system_settings', ['setting_key' => 'target_profit_rate', 'setting_value' => (string)$rate]);
}
\App\Core\SettingsHelper::clearCache();
$this->json(['status' => 'success']);
}
private function getGameId(): int {
if (!empty($_GET['game_id'])) return (int)$_GET['game_id'];
return (int)($this->db->get('games', 'id', ['code' => 'pk10']) ?: 0);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
<?php
namespace App\Controllers\Web;
use App\Core\WebBaseController;
use App\Core\I18n;
use App\Services\FollowPlanService;
use Db\Database;
class FollowPlanController extends WebBaseController {
public function followPlan() {
$this->checkWebLogin();
I18n::init();
$db = new Database();
$userId = $this->getCurrentUserId();
$user = $db->get('users', ['id','username','balance'], ['id' => $userId]);
$service = new FollowPlanService($db);
$game = $db->get('games', '*', ['code' => 'pk10', 'status' => 1]);
$gameId = $game['id'] ?? 0;
$plans = $service->getActivePlans($gameId);
$userFollows = $service->getUserFollowStatus($userId);
$summary = $service->getUserSummary($userId);
extract(compact('user', 'plans', 'userFollows', 'summary', 'game'));
include __DIR__ . '/../../Views/Web/follow_plan.php';
}
/**
* 切换跟单状态 API
*/
public function toggleFollow() {
$this->checkWebLogin();
header('Content-Type: application/json');
$db = new Database();
$data = json_decode(file_get_contents('php://input'), true);
$planId = intval($data['plan_id'] ?? 0);
$service = new FollowPlanService($db);
$result = $service->toggleFollow($this->getCurrentUserId(), $planId);
echo json_encode($result);
}
}
+24 -2
View File
@@ -29,10 +29,26 @@ class HomeController extends WebBaseController {
$user = $db->get('users', ['id','username','balance','lang'], ['id' => $userId]);
$game = $db->get('games', '*', ['code' => 'pk10', 'status' => 1]);
$isAgent = !!$db->get('agents', 'id', ['user_id' => $userId, 'status' => 1]);
// 加载赔率(优先代理专属赔率,否则用游戏默认赔率)
$gameId = $game['id'] ?? 0;
$oddsRaw = $db->select('game_odds', '*', ['game_id' => $gameId]);
$oddsMap = [];
foreach ($oddsRaw as $o) {
$oddsMap[$o['type'] . '_' . $o['target']] = (float)$o['odds'];
}
// 代理专属赔率覆盖
$agentId = $db->get('agents', 'id', ['user_id' => $userId, 'status' => 1]);
if ($agentId) {
$agentOdds = $db->select('agent_odds', '*', ['agent_id' => $agentId, 'game_id' => $gameId]);
foreach ($agentOdds as $ao) {
$oddsMap[$ao['bet_type'] . '_' . $ao['bet_target']] = (float)$ao['odds'];
}
}
extract([
'user' => $user ?: ['id' => $userId, 'username' => $this->getCurrentUsername(), 'balance' => 0],
'game' => $game,
'isAgent' => $isAgent,
'isAgent' => !!$agentId,
'oddsMap' => $oddsMap,
]);
include __DIR__ . '/../../Views/Web/pk10.php';
}
@@ -56,7 +72,13 @@ class HomeController extends WebBaseController {
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 20
]);
extract(compact('user', 'transactions', 'bets'));
// 充提记录
$fundRequests = $db->select('fund_requests', '*', [
'user_id' => $userId,
'ORDER' => ['id' => 'DESC'], 'LIMIT' => 20
]);
extract(compact('user', 'transactions', 'bets', 'fundRequests'));
include __DIR__ . '/../../Views/Web/profile.php';
}
+1 -1
View File
@@ -84,7 +84,7 @@ class PeriodController extends WebBaseController {
'game_id' => $gameId,
'status' => ['drawn', 'settled'],
'ORDER' => ['id' => 'DESC'],
'LIMIT' => 10
'LIMIT' => 30
]);
foreach ($recentPeriods as $s) {
$pk = $getResult($s);
@@ -0,0 +1,36 @@
<?php
namespace App\Controllers\Web;
use App\Core\WebBaseController;
use App\Core\I18n;
use App\Services\ReportService;
use Db\Database;
class ReportWebController extends WebBaseController {
/**
* 前台报表查询页面
*/
public function report() {
$this->checkWebLogin();
I18n::init();
$db = new Database();
$userId = $this->getCurrentUserId();
$user = $db->get('users', ['id','username','balance'], ['id' => $userId]);
extract(compact('user'));
include __DIR__ . '/../../Views/Web/report.php';
}
/**
* 前台报表查询 API
*/
public function userReportApi() {
$this->checkWebLogin();
header('Content-Type: application/json');
$service = new ReportService(new Database());
$dateFrom = $_GET['from'] ?? date('Y-m-d');
$dateTo = $_GET['to'] ?? date('Y-m-d');
$data = $service->getUserReport($this->getCurrentUserId(), $dateFrom, $dateTo);
echo json_encode(['success' => true, 'data' => $data]);
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Controllers\Web;
use App\Core\WebBaseController;
use App\Services\TransactionService;
use Db\Database;
class TransactionController extends WebBaseController {
public function fundRequest() {
$this->checkWebLogin();
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
$service = new TransactionService(new Database());
$result = $service->fundRequest(
$this->getCurrentUserId(),
$data['type'] ?? '',
floatval($data['amount'] ?? 0),
$data['remark'] ?? ''
);
echo json_encode($result);
}
public function transfer() {
$this->checkWebLogin();
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
$service = new TransactionService(new Database());
$result = $service->transfer(
$this->getCurrentUserId(),
trim($data['to_username'] ?? ''),
floatval($data['amount'] ?? 0)
);
echo json_encode($result);
}
}