Initial commit: 投注游戏平台初始化
This commit is contained in:
@@ -0,0 +1,455 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* =============================================
|
||||
* 自动开期 & 开奖 & 结算 统一定时任务
|
||||
* =============================================
|
||||
*
|
||||
* 【宝塔面板配置方法】
|
||||
* 1. 登录宝塔面板 → 计划任务
|
||||
* 2. 任务类型: Shell脚本
|
||||
* 3. 任务名称: PK10自动开期
|
||||
* 4. 执行周期: 每N分钟 → 1分钟
|
||||
* 5. 脚本内容:
|
||||
* cd /www/wwwroot/你的项目目录 && /usr/bin/php cron/auto_period_task.php >> Storage/log/auto_period.log 2>&1
|
||||
*
|
||||
* 或者直接填:
|
||||
* /usr/bin/php /www/wwwroot/你的项目目录/cron/auto_period_task.php
|
||||
*
|
||||
* 【工作流程】
|
||||
* 每分钟执行一次,对每个启用了自动开期的游戏:
|
||||
* 1. 检查是否有活跃期号
|
||||
* 2. 没有 → 开启新一期
|
||||
* 3. 有 → 检查是否到封盘时间 → 封盘
|
||||
* 4. 检查是否到开奖时间 → 开奖
|
||||
* 5. 已开奖 → 结算 → 开新一期
|
||||
*
|
||||
* 【安全机制】
|
||||
* - 文件锁防止并发执行
|
||||
* - 数据库事务保证原子性
|
||||
* - SELECT FOR UPDATE 防止并发扣款
|
||||
*/
|
||||
|
||||
// ============================
|
||||
// 环境初始化
|
||||
// ============================
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
|
||||
// 自动检测项目根目录
|
||||
$rootDir = dirname(__DIR__);
|
||||
chdir($rootDir);
|
||||
|
||||
// 加载依赖
|
||||
require_once $rootDir . '/Db/Database.php';
|
||||
require_once $rootDir . '/Db/Medoo.php';
|
||||
require_once $rootDir . '/App/Core/GameAlgorithmInterface.php';
|
||||
require_once $rootDir . '/App/Core/GameFactory.php';
|
||||
require_once $rootDir . '/App/Core/PK10Algorithm.php';
|
||||
require_once $rootDir . '/App/Core/DiceAlgorithm.php';
|
||||
require_once $rootDir . '/App/Core/XocDiaAlgorithm.php';
|
||||
|
||||
use Db\Database;
|
||||
use App\Core\GameFactory;
|
||||
|
||||
// ============================
|
||||
// 日志
|
||||
// ============================
|
||||
$logDir = $rootDir . '/Storage/log';
|
||||
if (!is_dir($logDir)) {
|
||||
@mkdir($logDir, 0755, true);
|
||||
}
|
||||
|
||||
function logInfo($msg) {
|
||||
echo '[' . date('Y-m-d H:i:s') . '] ' . $msg . PHP_EOL;
|
||||
}
|
||||
|
||||
function logToDb($db, $gameId, $periodId, $action, $message) {
|
||||
try {
|
||||
$db->insert('auto_period_log', [
|
||||
'game_id' => $gameId,
|
||||
'period_id' => $periodId,
|
||||
'action' => $action,
|
||||
'message' => $message,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// 日志表可能不存在,忽略
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 文件锁防并发
|
||||
// ============================
|
||||
$lockFile = $logDir . '/auto_period_task.lock';
|
||||
$lockHandle = fopen($lockFile, 'w');
|
||||
if (!flock($lockHandle, LOCK_EX | LOCK_NB)) {
|
||||
logInfo('⚠ 任务已在运行中,本次跳过');
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// 写入 PID
|
||||
fwrite($lockHandle, (string)getmypid());
|
||||
|
||||
logInfo('========== 自动开期任务开始 ==========');
|
||||
|
||||
try {
|
||||
$db = new Database();
|
||||
|
||||
// 获取所有启用自动开期的游戏
|
||||
$games = $db->select('games', '*', [
|
||||
'status' => 1,
|
||||
'auto_period_enabled' => 1
|
||||
]);
|
||||
|
||||
if (empty($games)) {
|
||||
logInfo('没有启用自动开期的游戏,退出');
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
logInfo('发现 ' . count($games) . ' 个启用自动开期的游戏');
|
||||
|
||||
foreach ($games as $game) {
|
||||
processGame($db, $game);
|
||||
}
|
||||
|
||||
logInfo('========== 自动开期任务完成 ==========');
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
logInfo('❌ 致命错误: ' . $e->getMessage());
|
||||
logInfo($e->getTraceAsString());
|
||||
}
|
||||
|
||||
cleanup:
|
||||
flock($lockHandle, LOCK_UN);
|
||||
fclose($lockHandle);
|
||||
@unlink($lockFile);
|
||||
exit(0);
|
||||
|
||||
// ============================
|
||||
// 处理单个游戏
|
||||
// ============================
|
||||
function processGame($db, $game) {
|
||||
$gameId = (int)$game['id'];
|
||||
$gameName = $game['name'];
|
||||
$gameType = $game['type'] ?? 'pk10';
|
||||
$periodDuration = max(60, (int)($game['period_duration'] ?? 300)); // 最少60秒
|
||||
$lockBeforeEnd = max(5, (int)($game['lock_before_end'] ?? 30)); // 最少5秒
|
||||
|
||||
logInfo("--- 处理游戏: [{$gameName}] (ID:{$gameId}, 类型:{$gameType}, 周期:{$periodDuration}秒) ---");
|
||||
|
||||
// 获取当前活跃期号
|
||||
$activePeriod = $db->get('periods', '*', [
|
||||
'game_id' => $gameId,
|
||||
'status' => ['pending', 'locked', 'drawn'],
|
||||
'ORDER' => ['id' => 'DESC']
|
||||
]);
|
||||
|
||||
if (!$activePeriod) {
|
||||
logInfo(" 无活跃期号,开启新一期");
|
||||
$newId = doStartNewPeriod($db, $game, $periodDuration);
|
||||
if ($newId) {
|
||||
logToDb($db, $gameId, $newId, 'start', '自动开启新期号');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$periodId = (int)$activePeriod['id'];
|
||||
$periodNum = $activePeriod['period_number'];
|
||||
$status = $activePeriod['status'];
|
||||
$startTime = strtotime($activePeriod['start_time'] ?? $activePeriod['created_at']);
|
||||
$elapsed = time() - $startTime;
|
||||
|
||||
logInfo(" 当前期号: {$periodNum} | 状态: {$status} | 已过 {$elapsed}/{$periodDuration} 秒");
|
||||
|
||||
// 获取算法类
|
||||
$algoClass = GameFactory::getAlgorithm($gameType);
|
||||
|
||||
// === 阶段1:封盘 + 预开奖 ===
|
||||
if ($status === 'pending' && $elapsed >= ($periodDuration - $lockBeforeEnd)) {
|
||||
logInfo(" → 执行封盘 + 预开奖");
|
||||
|
||||
$preResult = doPreDraw($db, $periodId, $gameId, $algoClass);
|
||||
|
||||
$updateData = ['status' => 'locked', 'updated_at' => date('Y-m-d H:i:s')];
|
||||
if ($preResult) {
|
||||
$updateData['result'] = json_encode($preResult);
|
||||
}
|
||||
$db->update('periods', $updateData, ['id' => $periodId]);
|
||||
|
||||
// 写入结果存储
|
||||
if ($preResult) {
|
||||
$resultTable = $algoClass::getResultTable();
|
||||
$storageData = $algoClass::formatResultForStorage($preResult, $periodId);
|
||||
if ($resultTable) {
|
||||
$db->delete($resultTable, ['period_id' => $periodId]);
|
||||
$db->insert($resultTable, $storageData);
|
||||
} else {
|
||||
$db->update('periods', $storageData, ['id' => $periodId]);
|
||||
}
|
||||
logInfo(" ✓ 预开奖: " . json_encode($preResult));
|
||||
}
|
||||
|
||||
logToDb($db, $gameId, $periodId, 'lock', '自动封盘+预开奖');
|
||||
$status = 'locked';
|
||||
}
|
||||
|
||||
// === 阶段2:正式开奖(仅更新状态为 drawn) ===
|
||||
if ($status === 'locked' && $elapsed >= $periodDuration) {
|
||||
logInfo(" → 正式标记开奖");
|
||||
|
||||
$existingResult = $db->get('periods', 'result', ['id' => $periodId]);
|
||||
if ($existingResult) {
|
||||
$db->update('periods', [
|
||||
'status' => 'drawn', 'draw_time' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'),
|
||||
], ['id' => $periodId]);
|
||||
logInfo(" ✓ 使用预开奖结果标记 drawn");
|
||||
} else {
|
||||
doDraw($db, $periodId, $gameId, $algoClass);
|
||||
}
|
||||
logToDb($db, $gameId, $periodId, 'draw', '自动开奖');
|
||||
$status = 'drawn';
|
||||
return;
|
||||
}
|
||||
|
||||
// === 阶段3:结算 + 开新期 ===
|
||||
if ($status === 'drawn') {
|
||||
$period = $db->get('periods', '*', ['id' => $periodId]);
|
||||
if ($period && $period['status'] === 'drawn') {
|
||||
logInfo(" → 执行结算");
|
||||
doSettle($db, $period, $algoClass, $gameType);
|
||||
logToDb($db, $gameId, $periodId, 'settle', '自动结算');
|
||||
|
||||
logInfo(" → 结算完毕,开新一期");
|
||||
$newId = doStartNewPeriod($db, $game, $periodDuration);
|
||||
if ($newId) {
|
||||
logToDb($db, $gameId, $newId, 'start', '结算后自动开启新期号');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 开启新一期
|
||||
// ============================
|
||||
function doStartNewPeriod($db, $game, $periodDuration) {
|
||||
$gameId = (int)$game['id'];
|
||||
$gameName = $game['name'];
|
||||
|
||||
// 生成期号
|
||||
$now = new \DateTime();
|
||||
$dateStr = $now->format('Ymd');
|
||||
$prefix = "G{$gameId}{$dateStr}";
|
||||
|
||||
try {
|
||||
$lastPeriod = $db->get('periods', 'period_number', [
|
||||
'period_number[~]' => $prefix . '%',
|
||||
'ORDER' => ['period_number' => 'DESC']
|
||||
]);
|
||||
|
||||
if ($lastPeriod) {
|
||||
$lastSeq = substr($lastPeriod, strlen($prefix));
|
||||
$newSeq = (int)$lastSeq + 1;
|
||||
$seqStr = str_pad((string)$newSeq, 4, '0', STR_PAD_LEFT);
|
||||
} else {
|
||||
$seqStr = '0001';
|
||||
}
|
||||
$periodNumber = $prefix . $seqStr;
|
||||
} catch (\Throwable $e) {
|
||||
$periodNumber = $prefix . str_pad((string)time() % 10000, 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
$streamUrl = !empty($game['stream_url']) ? $game['stream_url'] : null;
|
||||
|
||||
$periodData = [
|
||||
'period_number' => $periodNumber,
|
||||
'status' => 'pending',
|
||||
'game_id' => $gameId,
|
||||
'stream_url' => $streamUrl,
|
||||
'start_time' => date('Y-m-d H:i:s'),
|
||||
'end_time' => date('Y-m-d H:i:s', time() + $periodDuration),
|
||||
'auto_generated' => 1,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
try {
|
||||
$insertId = $db->insert('periods', $periodData);
|
||||
if ($insertId) {
|
||||
logInfo(" ✓ 新期号: {$periodNumber} (ID:{$insertId}) 时长:{$periodDuration}秒");
|
||||
return $insertId;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
logInfo(" ✗ 开期失败: " . $e->getMessage());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 通用预开奖
|
||||
// ============================
|
||||
function doPreDraw($db, $periodId, $gameId, $algoClass) {
|
||||
$waterConfig = [];
|
||||
$wc = $db->select('water_control', '*', ['game_id' => $gameId, 'enabled' => 1]);
|
||||
foreach ($wc as $w) {
|
||||
$waterConfig[$w['bet_type']] = (float)$w['win_rate_pct'];
|
||||
}
|
||||
|
||||
$bets = $db->select('bets', '*', [
|
||||
'period_id' => $periodId, 'status' => 'pending', 'is_virtual' => 0
|
||||
]);
|
||||
|
||||
if (!empty($waterConfig) && !empty($bets)) {
|
||||
return $algoClass::generateControlledResult($bets, $waterConfig);
|
||||
}
|
||||
return $algoClass::generateResult();
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 通用开奖(无预开奖时的 fallback)
|
||||
// ============================
|
||||
function doDraw($db, $periodId, $gameId, $algoClass) {
|
||||
$result = doPreDraw($db, $periodId, $gameId, $algoClass);
|
||||
$resultJson = json_encode($result);
|
||||
|
||||
$db->medoo->pdo->beginTransaction();
|
||||
try {
|
||||
$updateData = [
|
||||
'status' => 'drawn', 'result' => $resultJson,
|
||||
'draw_time' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$resultTable = $algoClass::getResultTable();
|
||||
$storageData = $algoClass::formatResultForStorage($result, $periodId);
|
||||
|
||||
if (!$resultTable) {
|
||||
// dice/xocdia: 合并到 periods 更新
|
||||
$updateData = array_merge($updateData, $storageData);
|
||||
}
|
||||
$db->update('periods', $updateData, ['id' => $periodId]);
|
||||
|
||||
if ($resultTable) {
|
||||
$db->delete($resultTable, ['period_id' => $periodId]);
|
||||
$db->insert($resultTable, $storageData);
|
||||
}
|
||||
|
||||
$db->medoo->pdo->commit();
|
||||
logInfo(" ✓ 开奖: " . $resultJson);
|
||||
} catch (\Throwable $e) {
|
||||
$db->medoo->pdo->rollBack();
|
||||
logInfo(" ✗ 开奖失败: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 通用结算
|
||||
// ============================
|
||||
function doSettle($db, $period, $algoClass, $gameType) {
|
||||
$periodId = (int)$period['id'];
|
||||
$result = json_decode($period['result'], true);
|
||||
if (!$result) {
|
||||
logInfo(" ✗ 结算: 无开奖结果");
|
||||
return;
|
||||
}
|
||||
|
||||
$bets = $db->select('bets', '*', ['period_id' => $periodId, 'status' => 'pending']);
|
||||
if (empty($bets)) {
|
||||
$db->update('periods', ['status' => 'settled', 'updated_at' => date('Y-m-d H:i:s')], ['id' => $periodId]);
|
||||
logInfo(" ✓ 结算: 无投注,直接结算");
|
||||
return;
|
||||
}
|
||||
|
||||
$wins = 0; $losses = 0; $totalPayout = 0;
|
||||
$gameName = ucfirst($gameType);
|
||||
|
||||
$db->medoo->pdo->beginTransaction();
|
||||
try {
|
||||
foreach ($bets as $bet) {
|
||||
$isWin = $algoClass::checkWin($result, $bet['bet_type'], $bet['bet_value']);
|
||||
$betAmount = (float)$bet['amount'];
|
||||
$odds = (float)$bet['odds'];
|
||||
|
||||
if ($isWin) {
|
||||
$winAmount = $betAmount * $odds;
|
||||
$payout = $betAmount + $winAmount;
|
||||
$db->update('bets', [
|
||||
'status' => 'win', 'win_amount' => $winAmount, 'settled_at' => date('Y-m-d H:i:s'),
|
||||
], ['id' => $bet['id']]);
|
||||
|
||||
if (!(int)$bet['is_virtual']) {
|
||||
$stmt = $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;
|
||||
|
||||
$db->update('users', ['balance' => $newBalance], ['id' => $bet['user_id']]);
|
||||
$db->insert('transactions', [
|
||||
'user_id' => $bet['user_id'], 'type' => 'win',
|
||||
'amount' => $payout, 'balance_before' => $user['balance'],
|
||||
'balance_after' => $newBalance, 'related_id' => $periodId,
|
||||
'description' => $gameName . ' Auto Win - ' . $period['period_number'],
|
||||
'is_virtual' => $bet['is_virtual'], 'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
$totalPayout += $payout;
|
||||
}
|
||||
$wins++;
|
||||
} else {
|
||||
$db->update('bets', [
|
||||
'status' => 'lose', 'win_amount' => 0, 'settled_at' => date('Y-m-d H:i:s'),
|
||||
], ['id' => $bet['id']]);
|
||||
$losses++;
|
||||
}
|
||||
}
|
||||
|
||||
doSettleAgentCommissions($db, $bets, $periodId);
|
||||
$db->update('periods', ['status' => 'settled', 'updated_at' => date('Y-m-d H:i:s')], ['id' => $periodId]);
|
||||
$db->medoo->pdo->commit();
|
||||
logInfo(" ✓ {$gameName}结算: 中{$wins}笔 负{$losses}笔 派奖{$totalPayout}");
|
||||
} catch (\Throwable $e) {
|
||||
$db->medoo->pdo->rollBack();
|
||||
logInfo(" ✗ {$gameName}结算失败: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// 代理佣金
|
||||
// ============================
|
||||
function doSettleAgentCommissions($db, $bets, $periodId) {
|
||||
$agentBets = [];
|
||||
foreach ($bets as $bet) {
|
||||
if ((int)$bet['is_virtual']) continue;
|
||||
$user = $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 = $db->get('agents', '*', ['id' => $agentId, 'status' => 1]);
|
||||
if (!$agent) continue;
|
||||
$commission = $totalBet * $agent['commission_rate'] / 100;
|
||||
if ($commission <= 0) continue;
|
||||
|
||||
$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 = $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;
|
||||
$db->update('users', ['balance' => $newBal], ['id' => $agentUserId]);
|
||||
$db->insert('transactions', [
|
||||
'user_id' => $agentUserId, 'type' => 'commission',
|
||||
'amount' => $commission, 'balance_before' => $au['balance'],
|
||||
'balance_after' => $newBal, 'related_id' => $periodId,
|
||||
'description' => 'Auto agent commission', 'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user