Files
pk10/App/Controllers/Admin/PK10PeriodController.php
T
li 4a94fe36cf feat: 重构代码架构 + 新增报表/跟单/输赢统计功能
- 拆分 HomeController → TransactionController, ReportWebController, FollowPlanController
- 新增 Service 层: TransactionService, ReportService, FollowPlanService
- pk10.php JS 抽离为 4 个独立文件 (sound/race/bet/poll)
- 前台新增报表查询页面 (/report) + 跟单计划页面 (/follow-plan)
- 后台新增跟单计划管理 + 用户输赢明细统计
- 封盘状态显示倒计时 (x:xx)
- 音效仅在开奖弹窗打开时播放
- 路由按模块分组整理
- autoload 支持 App\Services 命名空间
2026-03-27 18:41:06 +08:00

438 lines
18 KiB
PHP

<?php
namespace App\Controllers\Admin;
use App\Core\AdminBaseController;
use App\Core\PK10Algorithm;
use Db\Database;
class PK10PeriodController extends AdminBaseController {
private $db;
public function __construct(Database $db) {
$this->db = $db;
}
public function index() {
$this->checkLogin();
$gameId = $this->getGameId();
$periods = $this->db->select('periods', '*', [
'game_id' => $gameId,
'ORDER' => ['id' => 'DESC'],
'LIMIT' => 50
]);
// 附加pk10_results
foreach ($periods as &$p) {
$p['pk10'] = $this->db->get('pk10_results', '*', ['period_id' => $p['id']]);
}
$current = $this->db->get('periods', '*', [
'game_id' => $gameId,
'status[!]' => 'settled',
'ORDER' => ['id' => 'DESC']
]);
if ($current) {
$current['pk10'] = $this->db->get('pk10_results', '*', ['period_id' => $current['id']]);
}
$stats = [
'total' => $this->db->count('periods', ['game_id' => $gameId]),
'pending' => $this->db->count('periods', ['game_id' => $gameId, 'status' => 'pending']),
'drawn' => $this->db->count('periods', ['game_id' => $gameId, 'status' => 'drawn']),
'settled' => $this->db->count('periods', ['game_id' => $gameId, 'status' => 'settled']),
];
$this->render('Admin/pk10_periods.php', compact('periods', 'current', 'stats', 'gameId'));
}
public function start() {
$this->checkLogin();
$gameId = $this->getGameId();
// 检查是否有未结算期号
$active = $this->db->get('periods', 'id', [
'game_id' => $gameId,
'status[!]' => 'settled'
]);
if ($active) {
$this->json(['status' => 'error', 'message' => 'There is an active period. Please settle it first.']);
return;
}
$periodNumber = PK10Algorithm::generatePeriodNumber($gameId);
$createdAt = date('Y-m-d H:i:s');
$this->db->insert('periods', [
'game_id' => $gameId,
'period_number' => $periodNumber,
'status' => 'pending',
'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]);
}
public function lock() {
$this->checkLogin();
$data = json_decode(file_get_contents('php://input'), true);
$periodId = (int)($data['period_id'] ?? 0);
$period = $this->db->get('periods', '*', ['id' => $periodId]);
if (!$period || $period['status'] !== 'pending') {
$this->json(['status' => 'error', 'message' => 'Invalid period or status']);
return;
}
$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']);
}
public function draw() {
$this->checkLogin();
$data = json_decode(file_get_contents('php://input'), true);
$periodId = (int)($data['period_id'] ?? 0);
$manual = $data['manual'] ?? false;
$period = $this->db->get('periods', '*', ['id' => $periodId]);
if (!$period || !in_array($period['status'], ['pending', 'locked'])) {
$this->json(['status' => 'error', 'message' => 'Invalid period or status']);
return;
}
$gameId = $period['game_id'];
if ($manual && !empty($data['result'])) {
// 手动补开奖
$result = array_map('intval', $data['result']);
if (count($result) !== 10 || count(array_unique($result)) !== 10) {
$this->json(['status' => 'error', 'message' => 'Result must be 10 unique numbers 1-10']);
return;
}
} else {
// 自动开奖(带放水机制)
$waterConfig = [];
$wc = $this->db->select('water_control', '*', ['game_id' => $gameId, 'enabled' => 1]);
foreach ($wc as $w) {
$waterConfig[$w['bet_type']] = (float)$w['win_rate_pct'];
}
$bets = $this->db->select('bets', '*', [
'period_id' => $periodId,
'status' => 'pending',
'is_virtual' => 0
]);
if (!empty($waterConfig) && !empty($bets)) {
$targetProfitRate = floatval(\App\Core\SettingsHelper::get('target_profit_rate'));
$result = PK10Algorithm::generateControlledResult($bets, $waterConfig, 100, $targetProfitRate);
} else {
$result = PK10Algorithm::generateResult();
}
}
$sum = $result[0] + $result[1];
$resultJson = json_encode($result);
$this->db->medoo->pdo->beginTransaction();
try {
$this->db->update('periods', [
'status' => 'drawn',
'result' => $resultJson,
'draw_time' => date('Y-m-d H:i:s'),
], ['id' => $periodId]);
// 删除旧结果(如果手动修改)
$this->db->delete('pk10_results', ['period_id' => $periodId]);
$this->db->insert('pk10_results', [
'period_id' => $periodId,
'rank_1' => $result[0], 'rank_2' => $result[1], 'rank_3' => $result[2],
'rank_4' => $result[3], 'rank_5' => $result[4], 'rank_6' => $result[5],
'rank_7' => $result[6], 'rank_8' => $result[7], 'rank_9' => $result[8],
'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();
$this->json(['status' => 'error', 'message' => 'Draw failed: ' . $e->getMessage()]);
return;
}
$this->json(['status' => 'success', 'message' => 'Draw completed', 'result' => $result, 'sum' => $sum]);
}
public function settle() {
$this->checkLogin();
$data = json_decode(file_get_contents('php://input'), true);
$periodId = (int)($data['period_id'] ?? 0);
$period = $this->db->get('periods', '*', ['id' => $periodId]);
if (!$period || $period['status'] !== 'drawn') {
$this->json(['status' => 'error', 'message' => 'Period must be drawn first']);
return;
}
$result = json_decode($period['result'], true);
if (!$result || count($result) !== 10) {
$this->json(['status' => 'error', 'message' => 'Invalid result data']);
return;
}
$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;
}
$wins = 0; $losses = 0; $totalPayout = 0;
$this->db->medoo->pdo->beginTransaction();
try {
foreach ($bets as $bet) {
$isWin = PK10Algorithm::checkWin($result, $bet['bet_type'], $bet['bet_value']);
$betAmount = (float)$bet['amount'];
$odds = (float)$bet['odds'];
if ($isWin) {
$winAmount = $betAmount * $odds;
$payout = $betAmount + $winAmount;
$this->db->update('bets', [
'status' => 'win',
'win_amount' => $winAmount,
], ['id' => $bet['id']]);
$stmt = $this->db->medoo->pdo->prepare("SELECT balance FROM users WHERE id = :id FOR UPDATE");
$stmt->execute([':id' => $bet['user_id']]);
$user = $stmt->fetch(\PDO::FETCH_ASSOC);
$newBalance = (float)$user['balance'] + $payout;
$this->db->update('users', ['balance' => $newBalance], ['id' => $bet['user_id']]);
$this->db->insert('transactions', [
'user_id' => $bet['user_id'],
'type' => 'win',
'amount' => $payout,
'balance_before' => $user['balance'],
'balance_after' => $newBalance,
'related_id' => $periodId,
'description' => 'PK10 Win - Period ' . $period['period_number'],
'is_virtual' => $bet['is_virtual'],
'created_at' => date('Y-m-d H:i:s'),
]);
$totalPayout += $payout;
$wins++;
} else {
$this->db->update('bets', ['status' => 'lose', 'win_amount' => 0], ['id' => $bet['id']]);
$losses++;
}
}
// 计算代理佣金
$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();
$this->json(['status' => 'error', 'message' => 'Settle failed: ' . $e->getMessage()]);
return;
}
$this->json(['status' => 'success', 'message' => 'Settlement completed',
'wins' => $wins, 'losses' => $losses, 'total_payout' => $totalPayout]);
}
private function settleAgentCommissions(array $bets, int $periodId) {
$agentBets = [];
foreach ($bets as $bet) {
if ((int)$bet['is_virtual']) continue;
$user = $this->db->get('users', ['agent_id'], ['id' => $bet['user_id']]);
if (!$user || !$user['agent_id']) continue;
$agentId = $user['agent_id'];
if (!isset($agentBets[$agentId])) $agentBets[$agentId] = 0;
$agentBets[$agentId] += (float)$bet['amount'];
}
foreach ($agentBets as $agentId => $totalBet) {
$agent = $this->db->get('agents', '*', ['id' => $agentId, 'status' => 1]);
if (!$agent) continue;
$commission = $totalBet * $agent['commission_rate'] / 100;
if ($commission <= 0) continue;
$this->db->insert('agent_commissions', [
'agent_id' => $agentId,
'from_user_id' => 0,
'period_id' => $periodId,
'bet_amount' => $totalBet,
'commission' => $commission,
'type' => 'bet',
'created_at' => date('Y-m-d H:i:s'),
]);
// 佣金加到代理用户余额
$agentUserId = $agent['user_id'];
$stmt = $this->db->medoo->pdo->prepare("SELECT balance FROM users WHERE id = :id FOR UPDATE");
$stmt->execute([':id' => $agentUserId]);
$au = $stmt->fetch(\PDO::FETCH_ASSOC);
$newBal = (float)$au['balance'] + $commission;
$this->db->update('users', ['balance' => $newBal], ['id' => $agentUserId]);
$this->db->insert('transactions', [
'user_id' => $agentUserId, 'type' => 'commission',
'amount' => $commission, 'balance_before' => $au['balance'],
'balance_after' => $newBal, 'related_id' => $periodId,
'description' => 'Agent commission', 'created_at' => date('Y-m-d H:i:s'),
]);
}
}
public function get($id) {
$this->checkLogin();
$period = $this->db->get('periods', '*', ['id' => (int)$id]);
if ($period) {
$period['pk10'] = $this->db->get('pk10_results', '*', ['period_id' => $period['id']]);
}
$this->json(['status' => 'success', 'data' => $period]);
}
private function getGameId(): int {
$game = $this->db->get('games', 'id', ['code' => 'pk10']);
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);
}
}