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:
@@ -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,
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user