Initial commit: 投注游戏平台初始化
This commit is contained in:
Executable
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class AdminBaseController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// 检测是否是管理员
|
||||
protected function checkAdmin()
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
if (
|
||||
!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'
|
||||
) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => '您没有权限访问该功能'
|
||||
]);
|
||||
} else {
|
||||
echo "<script>alert('您没有权限访问该功能');history.back();</script>";
|
||||
}
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
class BaseController {
|
||||
|
||||
/**
|
||||
* 渲染视图
|
||||
* @param string $view 视图名,支持如 admin.dashboard(映射为 app/views/admin/dashboard.php)
|
||||
* @param array $data 传递给视图的数据
|
||||
*/
|
||||
protected function render($viewPath, $data = []) {
|
||||
if (strpos($viewPath, '/') === 0 || preg_match('/^[a-zA-Z]:\\\\/', $viewPath)) {
|
||||
$fullPath = $viewPath;
|
||||
} else {
|
||||
$viewsDir = __DIR__ . '/../Views/';
|
||||
$fullPath = $viewsDir . $viewPath;
|
||||
}
|
||||
|
||||
if (!file_exists($fullPath)) {
|
||||
throw new \Exception("视图文件不存在: {$fullPath}", 500);
|
||||
}
|
||||
|
||||
extract($data);
|
||||
|
||||
if (
|
||||
!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'
|
||||
) {
|
||||
include $fullPath;
|
||||
} else {
|
||||
ob_start();
|
||||
include $fullPath;
|
||||
$Content = ob_get_clean();
|
||||
|
||||
include __DIR__ . '/../Views/Admin/index.php'; // 主后台模板
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 检查用户登录
|
||||
*/
|
||||
protected function checkLogin() {
|
||||
|
||||
session_start();
|
||||
|
||||
$timeout = 7200;
|
||||
|
||||
// 提取 IP 前三段(IPv4)
|
||||
function get_ip_prefix($ip, $segments = 3) {
|
||||
$parts = explode('.', $ip);
|
||||
return implode('.', array_slice($parts, 0, $segments));
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['username'])) {
|
||||
header("Location: /admin/login");
|
||||
exit;
|
||||
}
|
||||
|
||||
// 宽松 IP 检查(只比对前三段,例如 192.168.1.xxx)
|
||||
$current_ip_prefix = get_ip_prefix($_SERVER['REMOTE_ADDR'], 3);
|
||||
$session_ip_prefix = get_ip_prefix($_SESSION['ip'] ?? '', 3);
|
||||
|
||||
if ($current_ip_prefix !== $session_ip_prefix) {
|
||||
session_destroy();
|
||||
header("Location: /admin/login");
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SESSION['ua'] !== $_SERVER['HTTP_USER_AGENT']) {
|
||||
session_destroy();
|
||||
header("Location: /admin/login");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (time() - ($_SESSION['last_activity'] ?? 0) > $timeout) {
|
||||
session_destroy();
|
||||
header("Location: /admin/login");
|
||||
exit;
|
||||
}
|
||||
$_SESSION['last_activity'] = time();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示 404 页面
|
||||
*/
|
||||
public function show404($msg = '') {
|
||||
http_response_code(404);
|
||||
echo "<h1>404 Not Found</h1>";
|
||||
if ($msg) echo "<p>$msg</p>";
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function showError($errorMessage) {
|
||||
// 错误视图文件路径(根据实际项目目录调整)
|
||||
$errorViewPath = __DIR__ . '/../Views/Web/error.php';
|
||||
|
||||
// 检查错误视图文件是否存在
|
||||
if (!file_exists($errorViewPath)) {
|
||||
die("错误:找不到错误视图文件,请检查路径是否正确");
|
||||
}
|
||||
|
||||
// 传递错误信息到视图
|
||||
$error = $errorMessage;
|
||||
|
||||
// 加载错误视图(通过include将变量传入视图)
|
||||
include $errorViewPath;
|
||||
|
||||
// 终止后续代码执行
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class DiceAlgorithm implements GameAlgorithmInterface {
|
||||
|
||||
public static function generateResult(): array {
|
||||
return [rand(1, 6), rand(1, 6), rand(1, 6)];
|
||||
}
|
||||
|
||||
public static function generateControlledResult(array $bets, array $waterConfig, int $attempts = 100): array {
|
||||
if (empty($bets) || empty($waterConfig)) return self::generateResult();
|
||||
$bestResult = null;
|
||||
$bestProfit = PHP_INT_MIN;
|
||||
for ($i = 0; $i < $attempts; $i++) {
|
||||
$result = self::generateResult();
|
||||
$profit = self::calculatePlatformProfit($result, $bets);
|
||||
if ($profit > $bestProfit) { $bestProfit = $profit; $bestResult = $result; }
|
||||
}
|
||||
return $bestResult;
|
||||
}
|
||||
|
||||
public static function checkWin(array $result, string $betType, string $betTarget): bool {
|
||||
$total = array_sum($result);
|
||||
switch ($betType) {
|
||||
case 'big_small':
|
||||
return ($betTarget === 'big' && $total >= 11) || ($betTarget === 'small' && $total <= 10);
|
||||
case 'odd_even':
|
||||
return ($betTarget === 'odd' && $total % 2 === 1) || ($betTarget === 'even' && $total % 2 === 0);
|
||||
case 'sum':
|
||||
return (int)$betTarget === $total;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function calculatePlatformProfit(array $result, array $bets): float {
|
||||
$totalBet = 0; $totalPayout = 0;
|
||||
foreach ($bets as $bet) {
|
||||
$amount = (float)$bet['amount'];
|
||||
$totalBet += $amount;
|
||||
if (self::checkWin($result, $bet['bet_type'], $bet['bet_target'] ?? $bet['bet_value'] ?? '')) {
|
||||
$totalPayout += $amount + $amount * (float)$bet['odds'];
|
||||
}
|
||||
}
|
||||
return $totalBet - $totalPayout;
|
||||
}
|
||||
|
||||
public static function getResultTable(): ?string { return null; }
|
||||
|
||||
public static function formatResultForStorage(array $result, int $periodId): array {
|
||||
return [
|
||||
'dice1' => $result[0], 'dice2' => $result[1], 'dice3' => $result[2],
|
||||
'total' => array_sum($result),
|
||||
];
|
||||
}
|
||||
|
||||
public static function parseResultFromDb(array $row): array {
|
||||
return [(int)($row['dice1'] ?? 0), (int)($row['dice2'] ?? 0), (int)($row['dice3'] ?? 0)];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
interface GameAlgorithmInterface {
|
||||
public static function generateResult(): array;
|
||||
public static function generateControlledResult(array $bets, array $waterConfig, int $attempts = 100): array;
|
||||
public static function checkWin(array $result, string $betType, string $betTarget): bool;
|
||||
public static function calculatePlatformProfit(array $result, array $bets): float;
|
||||
public static function getResultTable(): ?string;
|
||||
public static function formatResultForStorage(array $result, int $periodId): array;
|
||||
public static function parseResultFromDb(array $row): array;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class GameFactory {
|
||||
private static $map = [
|
||||
'pk10' => PK10Algorithm::class,
|
||||
'dice' => DiceAlgorithm::class,
|
||||
'xocdia' => XocDiaAlgorithm::class,
|
||||
];
|
||||
|
||||
public static function getAlgorithm(string $gameType): string {
|
||||
$class = self::$map[$gameType] ?? null;
|
||||
if (!$class) throw new \RuntimeException("Unknown game type: {$gameType}");
|
||||
return $class;
|
||||
}
|
||||
|
||||
public static function register(string $gameType, string $class): void {
|
||||
self::$map[$gameType] = $class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class I18n {
|
||||
private static $lang = 'en';
|
||||
private static $translations = [];
|
||||
private static $fallback = [];
|
||||
private static $loaded = false;
|
||||
|
||||
// 支持的语言列表
|
||||
const LANGUAGES = [
|
||||
'en' => 'English',
|
||||
'th' => 'ไทย',
|
||||
'vi' => 'Tiếng Việt',
|
||||
'zh' => '中文',
|
||||
'ms' => 'Bahasa Melayu',
|
||||
'fil' => 'Filipino',
|
||||
'bn' => 'বাংলা',
|
||||
];
|
||||
|
||||
public static function init($db = null) {
|
||||
if (self::$loaded) return;
|
||||
|
||||
// 优先级: URL参数 > Session > Cookie > 浏览器 > 默认en
|
||||
if (!empty($_GET['lang']) && isset(self::LANGUAGES[$_GET['lang']])) {
|
||||
self::$lang = $_GET['lang'];
|
||||
} elseif (!empty($_SESSION['lang'])) {
|
||||
self::$lang = $_SESSION['lang'];
|
||||
} elseif (!empty($_COOKIE['lang'])) {
|
||||
self::$lang = $_COOKIE['lang'];
|
||||
} else {
|
||||
self::$lang = self::detectBrowserLang();
|
||||
}
|
||||
|
||||
$_SESSION['lang'] = self::$lang;
|
||||
setcookie('lang', self::$lang, time() + 86400 * 365, '/');
|
||||
|
||||
self::loadFromFile();
|
||||
self::$loaded = true;
|
||||
}
|
||||
|
||||
private static function detectBrowserLang(): string {
|
||||
$accept = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
|
||||
foreach (self::LANGUAGES as $code => $name) {
|
||||
if (stripos($accept, $code) !== false) return $code;
|
||||
}
|
||||
return 'en';
|
||||
}
|
||||
|
||||
private static function loadFromFile() {
|
||||
$file = ROOT_PATH . 'Lang/' . self::$lang . '.php';
|
||||
if (file_exists($file)) {
|
||||
self::$translations = require $file;
|
||||
}
|
||||
// 始终加载英文作为fallback
|
||||
$enFile = ROOT_PATH . 'Lang/en.php';
|
||||
if (file_exists($enFile)) {
|
||||
self::$fallback = require $enFile;
|
||||
}
|
||||
}
|
||||
|
||||
public static function t(string $key, array $params = []): string {
|
||||
$text = self::$translations[$key] ?? self::$fallback[$key] ?? $key;
|
||||
foreach ($params as $k => $v) {
|
||||
$text = str_replace(':' . $k, $v, $text);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
public static function getLang(): string { return self::$lang; }
|
||||
public static function setLang(string $lang) {
|
||||
if (isset(self::LANGUAGES[$lang])) {
|
||||
self::$lang = $lang;
|
||||
self::$loaded = false;
|
||||
self::init();
|
||||
}
|
||||
}
|
||||
public static function getLanguages(): array { return self::LANGUAGES; }
|
||||
}
|
||||
|
||||
// 全局快捷函数
|
||||
function __($key, $params = []) { return I18n::t($key, $params); }
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
use Db\Database;
|
||||
|
||||
class Mailer {
|
||||
|
||||
public static function send(string $to, string $subject, string $htmlBody): bool {
|
||||
$cfg = self::getConfig();
|
||||
if (empty($cfg['smtp_host']) || empty($cfg['smtp_user'])) {
|
||||
// fallback to mail()
|
||||
$headers = "MIME-Version: 1.0\r\nContent-type:text/html;charset=UTF-8\r\nFrom: {$cfg['smtp_from']}\r\n";
|
||||
return @mail($to, $subject, $htmlBody, $headers);
|
||||
}
|
||||
|
||||
$host = $cfg['smtp_host'];
|
||||
$port = (int)($cfg['smtp_port'] ?: 465);
|
||||
$user = $cfg['smtp_user'];
|
||||
$pass = $cfg['smtp_pass'];
|
||||
$from = $cfg['smtp_from'] ?: $user;
|
||||
$fromName = $cfg['smtp_from_name'] ?: 'System';
|
||||
$encryption = $cfg['smtp_encryption'] ?: 'ssl';
|
||||
|
||||
try {
|
||||
$target = ($encryption === 'ssl') ? "ssl://{$host}" : $host;
|
||||
$sock = @fsockopen($target, $port, $errno, $errstr, 10);
|
||||
if (!$sock) throw new \RuntimeException("Connect failed: {$errstr}");
|
||||
|
||||
self::readLine($sock);
|
||||
self::cmd($sock, "EHLO localhost");
|
||||
|
||||
// STARTTLS for tls mode
|
||||
if ($encryption === 'tls') {
|
||||
self::cmd($sock, "STARTTLS");
|
||||
stream_socket_enable_crypto($sock, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
|
||||
self::cmd($sock, "EHLO localhost");
|
||||
}
|
||||
|
||||
// AUTH LOGIN
|
||||
self::cmd($sock, "AUTH LOGIN");
|
||||
self::cmd($sock, base64_encode($user));
|
||||
self::cmd($sock, base64_encode($pass));
|
||||
|
||||
self::cmd($sock, "MAIL FROM:<{$from}>");
|
||||
self::cmd($sock, "RCPT TO:<{$to}>");
|
||||
self::cmd($sock, "DATA");
|
||||
|
||||
$msg = "From: {$fromName} <{$from}>\r\n"
|
||||
. "To: {$to}\r\n"
|
||||
. "Subject: {$subject}\r\n"
|
||||
. "MIME-Version: 1.0\r\n"
|
||||
. "Content-Type: text/html; charset=UTF-8\r\n"
|
||||
. "\r\n"
|
||||
. $htmlBody . "\r\n.\r\n";
|
||||
fwrite($sock, $msg);
|
||||
self::readLine($sock);
|
||||
|
||||
self::cmd($sock, "QUIT");
|
||||
fclose($sock);
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
error_log("Mailer error: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static function cmd($sock, string $cmd): string {
|
||||
fwrite($sock, $cmd . "\r\n");
|
||||
return self::readLine($sock);
|
||||
}
|
||||
|
||||
private static function readLine($sock): string {
|
||||
$resp = '';
|
||||
while ($line = fgets($sock, 512)) {
|
||||
$resp .= $line;
|
||||
if (isset($line[3]) && $line[3] === ' ') break;
|
||||
}
|
||||
return $resp;
|
||||
}
|
||||
|
||||
public static function getConfig(): array {
|
||||
$defaults = [
|
||||
'smtp_host' => '', 'smtp_port' => '465', 'smtp_user' => '',
|
||||
'smtp_pass' => '', 'smtp_from' => '', 'smtp_from_name' => 'PK10',
|
||||
'smtp_encryption' => 'ssl',
|
||||
];
|
||||
try {
|
||||
$db = new Database();
|
||||
$rows = $db->select('system_settings', ['setting_key', 'setting_value'], [
|
||||
'setting_key[~]' => 'smtp_%'
|
||||
]);
|
||||
foreach ($rows as $r) $defaults[$r['setting_key']] = $r['setting_value'];
|
||||
} catch (\Throwable $e) {}
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
public static function test(string $to): array {
|
||||
$ok = self::send($to, 'SMTP Test', '<h2>SMTP configuration is working!</h2><p>Time: ' . date('Y-m-d H:i:s') . '</p>');
|
||||
return ['success' => $ok, 'message' => $ok ? 'Test email sent' : 'Failed to send, check SMTP settings'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class PK10Algorithm implements GameAlgorithmInterface {
|
||||
|
||||
/**
|
||||
* 生成随机开奖结果 (1-10的排列)
|
||||
*/
|
||||
public static function generateResult(): array {
|
||||
$cars = range(1, 10);
|
||||
shuffle($cars);
|
||||
return $cars; // index 0=冠军, 1=亚军, ..., 9=第十名
|
||||
}
|
||||
|
||||
/**
|
||||
* 带放水机制的开奖结果生成
|
||||
* @param array $bets 当期所有投注 [{bet_type, bet_target, amount, odds}, ...]
|
||||
* @param array $waterConfig [{bet_type => win_rate_pct}, ...]
|
||||
* @param int $attempts 最大尝试次数
|
||||
*/
|
||||
public static function generateControlledResult(array $bets, array $waterConfig, int $attempts = 100): array {
|
||||
if (empty($bets) || empty($waterConfig)) {
|
||||
return self::generateResult();
|
||||
}
|
||||
|
||||
$bestResult = null;
|
||||
$bestProfit = PHP_INT_MIN;
|
||||
|
||||
for ($i = 0; $i < $attempts; $i++) {
|
||||
$result = self::generateResult();
|
||||
$profit = self::calculatePlatformProfit($result, $bets);
|
||||
|
||||
// 选择平台利润最高的结果
|
||||
if ($profit > $bestProfit) {
|
||||
$bestProfit = $profit;
|
||||
$bestResult = $result;
|
||||
}
|
||||
}
|
||||
|
||||
return $bestResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算某个开奖结果下平台的利润
|
||||
*/
|
||||
public static function calculatePlatformProfit(array $result, array $bets): float {
|
||||
$totalBet = 0;
|
||||
$totalPayout = 0;
|
||||
foreach ($bets as $bet) {
|
||||
$amount = (float)$bet['amount'];
|
||||
$totalBet += $amount;
|
||||
if (self::checkWin($result, $bet['bet_type'], $bet['bet_target'])) {
|
||||
$totalPayout += $amount + $amount * (float)$bet['odds'];
|
||||
}
|
||||
}
|
||||
return $totalBet - $totalPayout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心中奖判定
|
||||
* @param array $result 开奖排名 [冠军车号, 亚军车号, ..., 第十名车号]
|
||||
* @param string $betType 投注类型: rank/bs/oe/dt/sum/sum_bs
|
||||
* @param string $betTarget 投注目标: rank1_5/rank1_big/dt1_dragon/sum_11/sum_big...
|
||||
*/
|
||||
public static function checkWin(array $result, string $betType, string $betTarget): bool {
|
||||
switch ($betType) {
|
||||
case 'rank': return self::checkRank($result, $betTarget);
|
||||
case 'bs': return self::checkBigSmall($result, $betTarget);
|
||||
case 'oe': return self::checkOddEven($result, $betTarget);
|
||||
case 'dt': return self::checkDragonTiger($result, $betTarget);
|
||||
case 'sum': return self::checkSum($result, $betTarget);
|
||||
case 'sum_bs': return self::checkSumBigSmall($result, $betTarget);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 名次投注: rank{N}_{carNo} 如 rank1_5 = 冠军是5号车
|
||||
private static function checkRank(array $r, string $target): bool {
|
||||
if (!preg_match('/^rank(\d+)_(\d+)$/', $target, $m)) return false;
|
||||
$pos = (int)$m[1] - 1; // 0-indexed
|
||||
$car = (int)$m[2];
|
||||
return isset($r[$pos]) && $r[$pos] === $car;
|
||||
}
|
||||
|
||||
// 大小: rank{N}_big/small, 车号>=6为大, <=5为小
|
||||
private static function checkBigSmall(array $r, string $target): bool {
|
||||
if (!preg_match('/^rank(\d+)_(big|small)$/', $target, $m)) return false;
|
||||
$pos = (int)$m[1] - 1;
|
||||
if (!isset($r[$pos])) return false;
|
||||
$car = $r[$pos];
|
||||
return $m[2] === 'big' ? $car >= 6 : $car <= 5;
|
||||
}
|
||||
|
||||
// 单双: rank{N}_odd/even
|
||||
private static function checkOddEven(array $r, string $target): bool {
|
||||
if (!preg_match('/^rank(\d+)_(odd|even)$/', $target, $m)) return false;
|
||||
$pos = (int)$m[1] - 1;
|
||||
if (!isset($r[$pos])) return false;
|
||||
$car = $r[$pos];
|
||||
return $m[2] === 'odd' ? ($car % 2 === 1) : ($car % 2 === 0);
|
||||
}
|
||||
|
||||
// 龙虎: dt{N}_dragon/tiger, N=1-5, 对应 1vs10, 2vs9, 3vs8, 4vs7, 5vs6
|
||||
private static function checkDragonTiger(array $r, string $target): bool {
|
||||
if (!preg_match('/^dt(\d)_(dragon|tiger)$/', $target, $m)) return false;
|
||||
$pair = (int)$m[1]; // 1-5
|
||||
$frontPos = $pair - 1; // 0,1,2,3,4
|
||||
$backPos = 10 - $pair; // 9,8,7,6,5
|
||||
if (!isset($r[$frontPos], $r[$backPos])) return false;
|
||||
$front = $r[$frontPos];
|
||||
$back = $r[$backPos];
|
||||
if ($front === $back) return false; // 和局(PK10不会出现)
|
||||
return $m[2] === 'dragon' ? $front > $back : $front < $back;
|
||||
}
|
||||
|
||||
// 冠亚和值: sum_{3-19}
|
||||
private static function checkSum(array $r, string $target): bool {
|
||||
if (!preg_match('/^sum_(\d+)$/', $target, $m)) return false;
|
||||
$sumVal = $r[0] + $r[1]; // 冠军+亚军
|
||||
return $sumVal === (int)$m[1];
|
||||
}
|
||||
|
||||
// 冠亚和大小单双: sum_big/small/odd/even, 和>=12为大, <=11为小
|
||||
private static function checkSumBigSmall(array $r, string $target): bool {
|
||||
$sumVal = $r[0] + $r[1];
|
||||
switch ($target) {
|
||||
case 'sum_big': return $sumVal >= 12;
|
||||
case 'sum_small': return $sumVal <= 11;
|
||||
case 'sum_odd': return $sumVal % 2 === 1;
|
||||
case 'sum_even': return $sumVal % 2 === 0;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取名次中文/英文名称
|
||||
*/
|
||||
public static function getRankName(int $pos): string {
|
||||
$names = [1=>'champion',2=>'runner_up',3=>'rank_n',4=>'rank_n',5=>'rank_n',
|
||||
6=>'rank_n',7=>'rank_n',8=>'rank_n',9=>'rank_n',10=>'rank_n'];
|
||||
return $names[$pos] ?? 'rank_n';
|
||||
}
|
||||
|
||||
/**
|
||||
* 龙虎对应关系
|
||||
*/
|
||||
public static function getDragonTigerPairs(): array {
|
||||
return [
|
||||
1 => [1, 10], // 第1名 vs 第10名
|
||||
2 => [2, 9],
|
||||
3 => [3, 8],
|
||||
4 => [4, 7],
|
||||
5 => [5, 6],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 期号生成
|
||||
*/
|
||||
public static function generatePeriodNumber(int $gameId): string {
|
||||
return 'PK' . $gameId . date('Ymd') . str_pad(mt_rand(1, 9999), 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
public static function getResultTable(): ?string { return 'pk10_results'; }
|
||||
|
||||
public static function formatResultForStorage(array $result, int $periodId): array {
|
||||
return [
|
||||
'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' => $result[0] + $result[1],
|
||||
];
|
||||
}
|
||||
|
||||
public static function parseResultFromDb(array $row): array {
|
||||
$r = [];
|
||||
for ($i = 1; $i <= 10; $i++) $r[] = (int)($row['rank_' . $i] ?? 0);
|
||||
return $r;
|
||||
}
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class PluginBaseController extends BaseController {
|
||||
/**
|
||||
* 渲染插件视图文件
|
||||
*
|
||||
* @param string $pluginName 插件名称(必须与插件目录一致)
|
||||
* @param string $viewFile 视图相对路径,如 'Admin/list.php'
|
||||
* @param array $data 传递给视图的变量数组
|
||||
*/
|
||||
protected function renderPluginView($pluginName, $viewFile, $data = []) {
|
||||
$viewPath = PLUGIN_PATH . $pluginName . '/Views/' . $viewFile;
|
||||
// 调用基类的渲染方法
|
||||
$this->render($viewPath, $data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
Executable
+682
@@ -0,0 +1,682 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class PluginManager {
|
||||
protected $db;
|
||||
protected $pluginDir;
|
||||
protected $loadedPlugins = [];
|
||||
protected $router;
|
||||
protected $logFile;
|
||||
protected $installedPluginsCache = null;
|
||||
protected $registeredRoutes = [];
|
||||
protected $plugins = null;
|
||||
protected $enabledPlugins = [];
|
||||
protected $systemRoutes = [];
|
||||
protected $enabledRoutes = [];
|
||||
private $lastError = '';
|
||||
|
||||
private function setError(string $msg) {
|
||||
$this->lastError = $msg;
|
||||
$this->log("[ERROR] $msg");
|
||||
}
|
||||
|
||||
public function getLastError(): string {
|
||||
return $this->lastError;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param object $db 数据库对象
|
||||
* @param object $router 路由对象
|
||||
* @param string $pluginDir 插件目录
|
||||
*/
|
||||
public function __construct($db, $router, string $pluginDir) {
|
||||
$this->db = $db;
|
||||
$this->router = $router;
|
||||
$this->pluginDir = rtrim($pluginDir, '/');
|
||||
|
||||
$rootDir = realpath(__DIR__ . '/../../');
|
||||
$logDir = $rootDir . '/Storage/log';
|
||||
|
||||
// 确保日志目录(如不存在则创建)
|
||||
if (!is_dir($logDir)) {
|
||||
if (!mkdir($logDir, 0755, true) && !is_dir($logDir)) {
|
||||
throw new \RuntimeException("无法创建日志目录: $logDir ,请检查权限");
|
||||
}
|
||||
}
|
||||
|
||||
// 检查目录可写性
|
||||
if (!is_writable($logDir)) {
|
||||
throw new \RuntimeException("日志目录不可写: $logDir ,请检查权限");
|
||||
}
|
||||
|
||||
$this->logFile = $logDir . '/plugin_manager.log';
|
||||
|
||||
// 检查日志文件大小并自动清理(大于2MB时)
|
||||
$this->cleanupLogFile(2); // 传入最大允许的MB数
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理日志日志文件清理
|
||||
* @param int $maxSizeMB 最大允许的文件大小(MB)
|
||||
*/
|
||||
private function cleanupLogFile(int $maxSizeMB) {
|
||||
// 检查文件是否存在
|
||||
if (!file_exists($this->logFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 转换MB为字节
|
||||
$maxSizeBytes = $maxSizeMB * 1024 * 1024;
|
||||
|
||||
// 获取当前文件大小
|
||||
$currentSize = filesize($this->logFile);
|
||||
|
||||
// 如果文件大小超过限制,清空文件
|
||||
if ($currentSize > $maxSizeBytes) {
|
||||
// 先备份当前日志内容(可选)
|
||||
$backupFile = $this->logFile . '.bak_' . date('YmdHis');
|
||||
copy($this->logFile, $backupFile);
|
||||
|
||||
// 清空日志文件
|
||||
file_put_contents($this->logFile, '');
|
||||
|
||||
// 记录清理日志
|
||||
$message = "[" . date('Y-m-d H:i:s') . "] 日志文件超过{$maxSizeMB}MB,已自动清理\n";
|
||||
file_put_contents($this->logFile, $message, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置系统核心路由
|
||||
* @param array $routes 格式: [['GET', '/path', 'handler'], ...]
|
||||
*/
|
||||
public function setSystemRoutes(array $routes): void {
|
||||
$this->systemRoutes = [];
|
||||
foreach ($routes as $route) {
|
||||
if (count($route) < 2) continue;
|
||||
[$method, $path] = $route;
|
||||
$key = strtoupper(trim($method)) . ' ' . trim($path);
|
||||
$this->systemRoutes[$key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册路由(系统+已启用插件)
|
||||
* @return array 路由键名数组
|
||||
*/
|
||||
public function getAllRegisteredRoutes(): array {
|
||||
return array_merge(
|
||||
array_keys($this->systemRoutes),
|
||||
array_keys($this->enabledRoutes)
|
||||
);
|
||||
}
|
||||
|
||||
public function getDB() {
|
||||
return $this->db;
|
||||
}
|
||||
|
||||
protected function log(string $msg, string $level = 'INFO'): void {
|
||||
$date = date('Y-m-d H:i:s');
|
||||
$logMsg = "[$date] [$level] $msg\n";
|
||||
$logDir = dirname($this->logFile);
|
||||
|
||||
try {
|
||||
if (is_dir($logDir) && is_writable($logDir)) {
|
||||
file_put_contents($this->logFile, $logMsg, FILE_APPEND);
|
||||
} else {
|
||||
error_log("PluginManager log directory not writable: $logDir");
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
error_log("Failed to write plugin log: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有扫描到的插件信息
|
||||
* @return array
|
||||
*/
|
||||
public function getAllPlugins(): array {
|
||||
$this->scanPlugins();
|
||||
return $this->plugins;
|
||||
}
|
||||
|
||||
public function scanPlugins() {
|
||||
if ($this->plugins !== null) {
|
||||
return $this->plugins;
|
||||
}
|
||||
|
||||
$this->log("scanPlugins called");
|
||||
$this->plugins = [];
|
||||
|
||||
if (!is_dir($this->pluginDir)) {
|
||||
mkdir($this->pluginDir, 0755, true);
|
||||
$this->log("Plugin directory created: {$this->pluginDir}");
|
||||
return $this->plugins;
|
||||
}
|
||||
|
||||
$dirs = scandir($this->pluginDir);
|
||||
foreach ($dirs as $dir) {
|
||||
if ($dir === '.' || $dir === '..') continue;
|
||||
|
||||
$pluginPath = $this->pluginDir . '/' . $dir;
|
||||
$pluginFile = $pluginPath . '/mian.php';
|
||||
|
||||
if (is_dir($pluginPath) && is_file($pluginFile)) {
|
||||
$pluginConfig = $this->getPluginInfo($pluginFile, $dir);
|
||||
|
||||
if (empty($pluginConfig)) {
|
||||
$this->log("[WARN] 插件 {$dir} 不规范,已跳过");
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->plugins[$dir] = array_merge([
|
||||
'dir' => $dir,
|
||||
'path' => $pluginPath,
|
||||
], $pluginConfig);
|
||||
|
||||
$this->log("Plugin found: $dir ({$pluginConfig['name']})");
|
||||
}
|
||||
}
|
||||
|
||||
return $this->plugins;
|
||||
}
|
||||
|
||||
|
||||
public function getAllPluginIcons(): array
|
||||
{
|
||||
$pluginDirs = array_filter(scandir(PLUGIN_PATH . '/'), function($dir) {
|
||||
return $dir !== '.' && $dir !== '..';
|
||||
});
|
||||
|
||||
$icons = [];
|
||||
foreach ($pluginDirs as $pluginDir) {
|
||||
$pluginFile = PLUGIN_PATH . "/{$pluginDir}/mian.php";
|
||||
if (file_exists($pluginFile)) {
|
||||
$info = include $pluginFile;
|
||||
$icons[$pluginDir] = $info['menus'][0]['icon'] ?? 'fa fa-plug';
|
||||
}
|
||||
}
|
||||
|
||||
return $icons;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从插件文件头部注释获取插件信息
|
||||
*/
|
||||
private function getPluginInfo(string $pluginFile, string $pluginDirName): array {
|
||||
$info = [];
|
||||
$arrayConfig = @include $pluginFile;
|
||||
if (!is_array($arrayConfig)) {
|
||||
$this->setError("[ERROR] Plugin file {$pluginFile} must return an array");
|
||||
return [];
|
||||
}
|
||||
$lines = file($pluginFile);
|
||||
if (!$lines) {
|
||||
$this->setError("[ERROR] Cannot read plugin file: {$pluginFile}");
|
||||
return [];
|
||||
}
|
||||
|
||||
// 读取前 30 行,兼容大部分注释头
|
||||
$header = implode('', array_slice($lines, 0, 30));
|
||||
//$this->log("Header of {$pluginFile}:\n" . $header);
|
||||
if (preg_match_all('/^\s*\*\s*([A-Za-z ]+):\s*(.+)$/m', $header, $matches, PREG_SET_ORDER)) {
|
||||
foreach ($matches as $match) {
|
||||
$key = strtolower(str_replace(' ', '_', trim($match[1])));
|
||||
$value = trim($match[2]);
|
||||
$info[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// 必填字段列表
|
||||
$requiredFields = ['plugin_name', 'version', 'description', 'author', 'plugin_url'];
|
||||
|
||||
// 检查必要字段是否存在且非空
|
||||
foreach ($requiredFields as $field) {
|
||||
if (empty($info[$field])) {
|
||||
$this->setError("[ERROR] Plugin {$pluginFile} 缺少必要字段或为空: {$field}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 插件文件夹名必须和插件名一致
|
||||
if ($pluginDirName !== $info['plugin_name']) {
|
||||
$this->setError("[ERROR] 插件目录名 {$pluginDirName} 与插件名 {$info['plugin_name']} 不一致");
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (isset($info['plugin_name'])) {
|
||||
$arrayConfig['name'] = $info['plugin_name'];
|
||||
}
|
||||
if (isset($info['description'])) {
|
||||
$arrayConfig['description'] = $info['description'];
|
||||
}
|
||||
if (isset($info['version'])) {
|
||||
$arrayConfig['version'] = $info['version'];
|
||||
}
|
||||
if (isset($info['author'])) {
|
||||
$arrayConfig['author'] = $info['author'];
|
||||
}
|
||||
if (isset($info['plugin_url'])) {
|
||||
$arrayConfig['url'] = $info['plugin_url'];
|
||||
}
|
||||
|
||||
return $arrayConfig;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function clearCache() {
|
||||
$this->plugins = null;
|
||||
}
|
||||
|
||||
public function getEnabledPluginMenus(): array {
|
||||
$menus = [];
|
||||
|
||||
foreach ($this->enabledPlugins as $plugin) {
|
||||
if (isset($plugin['menus']) && is_array($plugin['menus'])) {
|
||||
$menus = array_merge($menus, $plugin['menus']);
|
||||
}
|
||||
}
|
||||
|
||||
return $menus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已安装插件列表
|
||||
* @return array
|
||||
*/
|
||||
public function getInstalledPlugins(): array {
|
||||
// 如果数据库对象不存在或未初始化,直接返回空数组
|
||||
if (!$this->db || !$this->db->medoo) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($this->installedPluginsCache !== null) {
|
||||
return $this->installedPluginsCache;
|
||||
}
|
||||
|
||||
try {
|
||||
$installed = $this->db->select('plugins', '*');
|
||||
if (!is_array($installed)) {
|
||||
$installed = [];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// 捕获数据库异常,返回空数组
|
||||
$installed = [];
|
||||
}
|
||||
|
||||
$installedPlugins = [];
|
||||
foreach ($installed as $row) {
|
||||
$installedPlugins[$row['name']] = $row;
|
||||
}
|
||||
|
||||
$this->installedPluginsCache = $installedPlugins;
|
||||
return $installedPlugins;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取所有插件状态信息
|
||||
* @return array
|
||||
*/
|
||||
public function getPluginStatusList(): array {
|
||||
$this->scanPlugins();
|
||||
$installedPlugins = $this->getInstalledPlugins();
|
||||
$result = [];
|
||||
foreach ($this->plugins as $name => $plugin) {
|
||||
$installed = isset($installedPlugins[$name]);
|
||||
$status = $installed ? (int)$installedPlugins[$name]['status'] : 0;
|
||||
$result[] = [
|
||||
'name' => $name,
|
||||
'installed' => $installed ? 1 : 0,
|
||||
'status' => $status,
|
||||
'path' => $plugin['path'],
|
||||
'title' => $installed ? $installedPlugins[$name]['title'] : ($plugin['title'] ?? $name),
|
||||
'version' => $installed ? $installedPlugins[$name]['version'] : ($plugin['version'] ?? ''),
|
||||
'description' => $installed ? $installedPlugins[$name]['description'] : ($plugin['description'] ?? ''),
|
||||
'author' => $installed ? $installedPlugins[$name]['author'] : ($plugin['author'] ?? ''),
|
||||
'url' => $installed ? $installedPlugins[$name]['url'] : ($plugin['url'] ?? ''),
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用的插件名称列表
|
||||
* @return array
|
||||
*/
|
||||
public function getEnabledPlugins(): array {
|
||||
$enabled = [];
|
||||
$installed = $this->getInstalledPlugins();
|
||||
foreach ($this->plugins as $name => $plugin) {
|
||||
if (isset($installed[$name]) && $installed[$name]['status'] == 1) {
|
||||
$enabled[] = $name;
|
||||
}
|
||||
}
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归加载控制器目录内所有PHP文件
|
||||
* @param string $dir
|
||||
*/
|
||||
protected function loadControllersRecursively(string $dir): void {
|
||||
if (!is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = scandir($dir);
|
||||
foreach ($files as $file) {
|
||||
if ($file === '.' || $file === '..') continue;
|
||||
|
||||
$fullPath = $dir . DIRECTORY_SEPARATOR . $file;
|
||||
if (is_dir($fullPath)) {
|
||||
$this->loadControllersRecursively($fullPath);
|
||||
} elseif (is_file($fullPath) && pathinfo($file, PATHINFO_EXTENSION) === 'php') {
|
||||
require_once $fullPath;
|
||||
$this->log("Loaded controller: $fullPath");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function isAssoc(array $arr): bool {
|
||||
return array_keys($arr) !== range(0, count($arr) - 1);
|
||||
}
|
||||
|
||||
public function loadEnabledPlugins(): void {
|
||||
static $loaded = false;
|
||||
if ($loaded) {
|
||||
return;
|
||||
}
|
||||
$loaded = true;
|
||||
|
||||
$this->scanPlugins();
|
||||
$installed = $this->getInstalledPlugins();
|
||||
|
||||
$this->enabledPlugins = [];
|
||||
$this->registeredRoutes = [];
|
||||
$this->enabledRoutes = [];
|
||||
|
||||
foreach ($this->plugins as $pluginName => $pluginData) {
|
||||
if (isset($installed[$pluginName]) && $installed[$pluginName]['status'] == 1) {
|
||||
// 激活插件(调用init)
|
||||
if (!empty($pluginData['init']) && is_callable($pluginData['init'])) {
|
||||
try {
|
||||
call_user_func($pluginData['init']);
|
||||
//$this->log("Initialized plugin $pluginName");
|
||||
} catch (\Throwable $e) {
|
||||
$this->log("[ERROR] Exception in init of plugin $pluginName: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 注册普通路由
|
||||
if (!empty($pluginData['routes']) && is_array($pluginData['routes'])) {
|
||||
foreach ($pluginData['routes'] as $route) {
|
||||
if (count($route) < 3) {
|
||||
$this->log("[WARNING] Invalid route config for plugin $pluginName");
|
||||
continue;
|
||||
}
|
||||
[$method, $path, $handler] = $route;
|
||||
$method = strtoupper($method);
|
||||
$routeKey = $method . ' ' . $path;
|
||||
|
||||
if (in_array($routeKey, $this->registeredRoutes, true)) {
|
||||
$this->log("[WARNING] Route conflict: [$method] $path");
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->router->add($method, $path, $handler);
|
||||
$this->registeredRoutes[] = $routeKey;
|
||||
$this->enabledRoutes[$routeKey] = true;
|
||||
//$this->log("Registered route [$method] $path for plugin $pluginName");
|
||||
}
|
||||
}
|
||||
|
||||
// 注册路由组
|
||||
if (!empty($pluginData['route_group'])) {
|
||||
$groups = $this->isAssoc($pluginData['route_group'])
|
||||
? [$pluginData['route_group']]
|
||||
: $pluginData['route_group'];
|
||||
|
||||
foreach ($groups as $group) {
|
||||
if (isset($group['prefix'], $group['namespace'], $group['routes']) && is_array($group['routes'])) {
|
||||
$this->router->group(
|
||||
$group['prefix'],
|
||||
$group['namespace'],
|
||||
$group['routes']
|
||||
);
|
||||
//$this->log("Registered route group [prefix={$group['prefix']}] for plugin $pluginName");
|
||||
} else {
|
||||
$this->log("[WARNING] Invalid route_group config in plugin $pluginName");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->enabledPlugins[] = $pluginData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 安装插件
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function installPlugin(string $name): bool|string {
|
||||
if (!isset($this->plugins[$name])) {
|
||||
$this->log("Install failed: plugin $name not found");
|
||||
return false;
|
||||
}
|
||||
$plugin = $this->plugins[$name];
|
||||
|
||||
// 1. 路由冲突检测
|
||||
if (!empty($plugin['route_group']) && is_array($plugin['route_group'])) {
|
||||
$prefixes = [];
|
||||
foreach ($plugin['route_group'] as $group) {
|
||||
if (isset($group['prefix'])) {
|
||||
$prefixes[] = $group['prefix'];
|
||||
}
|
||||
}
|
||||
$prefixes = array_unique($prefixes);
|
||||
|
||||
if (!$this->checkRouteConflicts($prefixes)) {
|
||||
return false; // 路由冲突阻止安装
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 表冲突检测
|
||||
if (!empty($plugin['tables']) && is_array($plugin['tables'])) {
|
||||
$conflictTables = [];
|
||||
|
||||
foreach ($plugin['tables'] as $table) {
|
||||
$stmt = $this->db->query("SHOW TABLES LIKE '{$table}'");
|
||||
if ($stmt && $stmt->fetch()) {
|
||||
$conflictTables[] = $table;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($conflictTables)) {
|
||||
$conflictList = implode(', ', $conflictTables);
|
||||
$errorMsg = "插件 {$name} 安装失败:以下数据表已存在 -> {$conflictList}";
|
||||
$this->log("[ERROR] $errorMsg");
|
||||
return $errorMsg; // 返回错误消息字符串阻止安装
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 插件元信息写入数据库
|
||||
$pluginData = [
|
||||
'name' => $name,
|
||||
'title' => $plugin['title'] ?? $name,
|
||||
'description' => $plugin['description'] ?? '',
|
||||
'version' => $plugin['version'] ?? '',
|
||||
'author' => $plugin['author'] ?? '',
|
||||
'url' => $plugin['url'] ?? '',
|
||||
'status' => 1,
|
||||
];
|
||||
|
||||
$count = $this->db->count('plugins', '*', ['name' => $name]);
|
||||
if ($count > 0) {
|
||||
$this->db->update('plugins', $pluginData, ['name' => $name]);
|
||||
} else {
|
||||
$this->db->insert('plugins', $pluginData);
|
||||
}
|
||||
|
||||
$this->installedPluginsCache = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 路由冲突检测
|
||||
*/
|
||||
public function checkRouteConflicts(array $newPrefixes): bool {
|
||||
$installedPlugins = $this->getInstalledPlugins();
|
||||
$installedPrefixes = [];
|
||||
|
||||
foreach ($this->plugins as $pluginName => $plugin) {
|
||||
if (!isset($installedPlugins[$pluginName])) continue;
|
||||
|
||||
if (!empty($plugin['route_group']) && is_array($plugin['route_group'])) {
|
||||
if (isset($plugin['route_group'][0]) && is_array($plugin['route_group'][0])) {
|
||||
foreach ($plugin['route_group'] as $group) {
|
||||
$prefix = $group['prefix'] ?? '';
|
||||
if ($prefix) $installedPrefixes[] = $prefix;
|
||||
}
|
||||
} else {
|
||||
$prefix = $plugin['route_group']['prefix'] ?? '';
|
||||
if ($prefix) $installedPrefixes[] = $prefix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$installedPrefixes = array_unique($installedPrefixes);
|
||||
$conflictNewPrefixes = [];
|
||||
$conflictInstalledPrefixes = [];
|
||||
|
||||
foreach ($newPrefixes as $newPrefix) {
|
||||
foreach ($installedPrefixes as $installedPrefix) {
|
||||
if ($this->isPrefixConflict($newPrefix, $installedPrefix)) {
|
||||
$conflictNewPrefixes[] = $newPrefix;
|
||||
$conflictInstalledPrefixes[] = $installedPrefix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($conflictNewPrefixes)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => "路由前缀冲突:新插件的前缀(" . implode(', ', array_unique($conflictNewPrefixes)) . ")与已安装插件的前缀(" . implode(', ', array_unique($conflictInstalledPrefixes)) . ")重复,请修改后再安装。"
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 判断两个路由前缀是否冲突
|
||||
*/
|
||||
protected function isPrefixConflict(string $prefixA, string $prefixB): bool {
|
||||
$prefixA = rtrim(trim($prefixA), '/');
|
||||
$prefixB = rtrim(trim($prefixB), '/');
|
||||
|
||||
// 两个都是空字符串,视为冲突
|
||||
if ($prefixA === '' && $prefixB === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 完全相等才算冲突
|
||||
if ($prefixA === $prefixB) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 不再判断包含关系为冲突,直接返回不冲突
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 卸载插件
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function uninstallPlugin(string $name): bool {
|
||||
if (!isset($this->plugins[$name])) {
|
||||
$this->log("Uninstall failed: plugin $name not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
$plugin = $this->plugins[$name];
|
||||
|
||||
// 删除插件相关的数据库表
|
||||
if (!empty($plugin['tables']) && is_array($plugin['tables'])) {
|
||||
foreach ($plugin['tables'] as $table) {
|
||||
$exists = $this->db->query("SHOW TABLES LIKE '{$table}'")->fetch();
|
||||
if ($exists) {
|
||||
try {
|
||||
$this->db->query("DROP TABLE IF EXISTS `{$table}`");
|
||||
$this->log("Dropped table: {$table}");
|
||||
} catch (\Throwable $e) {
|
||||
$this->log("[ERROR] Failed to drop table {$table}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 删除插件记录
|
||||
$this->db->delete('plugins', ['name' => $name]);
|
||||
|
||||
// 清除已安装插件缓存
|
||||
$this->installedPluginsCache = null;
|
||||
|
||||
$this->log("Uninstalled plugin {$name}");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 启用插件
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function enablePlugin(string $name): bool {
|
||||
$count = $this->db->count('plugins', '*', ['name' => $name]);
|
||||
if ($count == 0) {
|
||||
$this->log("Enable failed: plugin $name not installed");
|
||||
return false;
|
||||
}
|
||||
$this->db->update('plugins', ['status' => 1], ['name' => $name]);
|
||||
$this->installedPluginsCache = null;
|
||||
$this->log("Enabled plugin $name");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用插件
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function disablePlugin(string $name): bool {
|
||||
$count = $this->db->count('plugins', '*', ['name' => $name]);
|
||||
if ($count == 0) {
|
||||
$this->log("Disable failed: plugin $name not installed");
|
||||
return false;
|
||||
}
|
||||
$this->db->update('plugins', ['status' => 0], ['name' => $name]);
|
||||
$this->installedPluginsCache = null;
|
||||
$this->log("Disabled plugin $name");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Executable
+226
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class Router {
|
||||
private array $routes = [];
|
||||
private array $dependencies = [];
|
||||
|
||||
private function handle404() {
|
||||
$controller = new BaseController();
|
||||
$controller->show404();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加路由
|
||||
* @param string $method 请求方法 GET/POST 等
|
||||
* @param string $path 路径,必须以 / 开头,尾部无 /
|
||||
* @param callable|string $callback 处理器,格式:Controller@method 或回调函数
|
||||
*/
|
||||
|
||||
public function add($method, $path, $callback) {
|
||||
$methods = explode('|', strtoupper($method)); // 支持多方法
|
||||
$normalizedPath = $this->normalizePath($path);
|
||||
|
||||
foreach ($methods as $m) {
|
||||
$this->routes[$m][$normalizedPath] = $callback;
|
||||
}
|
||||
}
|
||||
|
||||
public function get($path, $callback) {
|
||||
$this->add('GET', $path, $callback);
|
||||
}
|
||||
|
||||
public function post($path, $callback) {
|
||||
$this->add('POST', $path, $callback);
|
||||
}
|
||||
|
||||
public function hasRoute($method, $path): bool {
|
||||
$method = strtoupper($method);
|
||||
$normalizedPath = $this->normalizePath($path);
|
||||
return isset($this->routes[$method][$normalizedPath]);
|
||||
}
|
||||
|
||||
|
||||
public function setDependencies(array $deps) {
|
||||
$this->dependencies = $deps;
|
||||
}
|
||||
/**
|
||||
* 路由分发
|
||||
* @param string $method 请求方法
|
||||
* @param string $uri 请求路径
|
||||
* @param array $dependencies 依赖注入数组,键为类名,值为实例(可选)
|
||||
*/
|
||||
public function dispatch($method, $uri, $dependencies = []) {
|
||||
$dependencies = array_merge($this->dependencies, $dependencies);
|
||||
$method = strtoupper($method);
|
||||
$normalizedPath = $this->normalizePath($uri);
|
||||
|
||||
if (!isset($this->routes[$method])) {
|
||||
header("HTTP/1.1 404 Not Found");
|
||||
echo "请求方法 [$method] 无任何注册路由。";
|
||||
return;
|
||||
}
|
||||
|
||||
// 精确匹配
|
||||
if (isset($this->routes[$method][$normalizedPath])) {
|
||||
$this->callHandler($this->routes[$method][$normalizedPath], [], $dependencies);
|
||||
return;
|
||||
}
|
||||
|
||||
// 模糊匹配带参数路由
|
||||
foreach ($this->routes[$method] as $routePath => $callback) {
|
||||
$pattern = $this->convertToRegex($routePath);
|
||||
if (preg_match($pattern, $normalizedPath, $matches)) {
|
||||
array_shift($matches); // 去掉完整匹配
|
||||
$this->callHandler($callback, $matches, $dependencies);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 404
|
||||
$this->handle404();
|
||||
}
|
||||
|
||||
public function group(string $prefixUri, string $controllerNamespace, array $routes)
|
||||
{
|
||||
foreach ($routes as $route) {
|
||||
if (count($route) < 3) {
|
||||
throw new \InvalidArgumentException("每个子路由必须包含 method、uri、handler");
|
||||
}
|
||||
|
||||
[$method, $subUri, $handler] = $route;
|
||||
|
||||
// 构造完整 URI
|
||||
$uri = rtrim($prefixUri, '/') . '/' . ltrim($subUri, '/');
|
||||
|
||||
// 构造完整处理器(加命名空间)
|
||||
if (strpos($handler, '@') !== false) {
|
||||
[$controller, $action] = explode('@', $handler);
|
||||
$fullHandler = $controllerNamespace . '\\' . $controller . '@' . $action;
|
||||
} else {
|
||||
$fullHandler = $controllerNamespace . '\\' . $handler;
|
||||
}
|
||||
|
||||
$this->add($method, $uri, $fullHandler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用处理器,支持构造函数依赖注入
|
||||
* @param callable|string $callback
|
||||
* @param array $params 传给方法的参数
|
||||
* @param array $dependencies 依赖注入映射,key: 类名,value: 实例
|
||||
*/
|
||||
private function callHandler($callback, $params = [], $dependencies = []) {
|
||||
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? '未知请求方法';
|
||||
$requestUri = $_SERVER['REQUEST_URI'] ?? '未知请求路径';
|
||||
|
||||
if (is_string($callback) && strpos($callback, '@') !== false) {
|
||||
list($class, $method) = explode('@', $callback);
|
||||
|
||||
if (class_exists($class) && method_exists($class, $method)) {
|
||||
try {
|
||||
$reflection = new \ReflectionClass($class);
|
||||
$instance = null;
|
||||
$constructor = $reflection->getConstructor();
|
||||
|
||||
if ($constructor) {
|
||||
$ctorParams = $constructor->getParameters();
|
||||
$args = [];
|
||||
|
||||
foreach ($ctorParams as $param) {
|
||||
$paramType = $param->getType();
|
||||
if ($paramType && !$paramType->isBuiltin()) {
|
||||
$paramClassName = $paramType->getName();
|
||||
if (isset($dependencies[$paramClassName])) {
|
||||
$args[] = $dependencies[$paramClassName];
|
||||
} elseif ($param->isDefaultValueAvailable()) {
|
||||
$args[] = $param->getDefaultValue();
|
||||
} else {
|
||||
throw new \Exception("依赖注入失败:未提供 {$paramClassName} 实例");
|
||||
}
|
||||
} else {
|
||||
$args[] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null;
|
||||
}
|
||||
}
|
||||
|
||||
$instance = $reflection->newInstanceArgs($args);
|
||||
} else {
|
||||
$instance = new $class();
|
||||
}
|
||||
|
||||
// ⚠️ 此处加入详细异常捕获
|
||||
try {
|
||||
call_user_func_array([$instance, $method], $params);
|
||||
} catch (\Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo "处理错误:<br>";
|
||||
echo "<strong>" . htmlspecialchars($e->getMessage()) . "</strong><br>";
|
||||
echo "文件:" . $e->getFile() . " 第 " . $e->getLine() . " 行<br>";
|
||||
echo "<pre>" . $e->getTraceAsString() . "</pre>";
|
||||
exit;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo "控制器初始化错误:<br>";
|
||||
echo "<strong>" . htmlspecialchars($e->getMessage()) . "</strong><br>";
|
||||
echo "文件:" . $e->getFile() . " 第 " . $e->getLine() . " 行<br>";
|
||||
echo "<pre>" . $e->getTraceAsString() . "</pre>";
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
http_response_code(500);
|
||||
echo "处理错误:类 <strong>" . htmlspecialchars($class) . "</strong> 或方法 <strong>" . htmlspecialchars($method) . "</strong> 未找到。<br>";
|
||||
echo "请求方法:<strong>{$requestMethod}</strong><br>";
|
||||
echo "请求路径:<strong>{$requestUri}</strong><br>";
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_callable($callback)) {
|
||||
try {
|
||||
call_user_func_array($callback, $params);
|
||||
} catch (\Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo "回调执行错误:<br>";
|
||||
echo "<strong>" . htmlspecialchars($e->getMessage()) . "</strong><br>";
|
||||
echo "文件:" . $e->getFile() . " 第 " . $e->getLine() . " 行<br>";
|
||||
echo "<pre>" . $e->getTraceAsString() . "</pre>";
|
||||
exit;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
http_response_code(500);
|
||||
echo "无效的路由处理器。<br>";
|
||||
echo "请求方法:<strong>{$requestMethod}</strong><br>";
|
||||
echo "请求路径:<strong>{$requestUri}</strong><br>";
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化路径,保证统一格式:
|
||||
* - 开头带 /
|
||||
* - 尾部无 /
|
||||
* - 根路径保持 /
|
||||
*/
|
||||
private function normalizePath($path) {
|
||||
$path = trim($path);
|
||||
if ($path === '' || $path === '/') {
|
||||
return '/';
|
||||
}
|
||||
return '/' . trim($path, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将路由路径转为正则表达式,支持 {param} 动态参数
|
||||
* @param string $routePath
|
||||
* @return string 正则表达式
|
||||
*/
|
||||
private function convertToRegex($routePath) {
|
||||
$pattern = preg_replace('/\{[a-zA-Z0-9_]+\}/', '([^/]+)', $routePath);
|
||||
return '/^' . str_replace('/', '\/', $pattern) . '$/';
|
||||
}
|
||||
}
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
use Db\Database;
|
||||
|
||||
class SettingsHelper {
|
||||
private static $settings = null;
|
||||
|
||||
/**
|
||||
* 获取所有系统设置
|
||||
*/
|
||||
public static function getAll() {
|
||||
if (self::$settings !== null) {
|
||||
return self::$settings;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = new Database();
|
||||
|
||||
// 尝试查询设置(如果表不存在会抛出异常,被catch捕获)
|
||||
try {
|
||||
$settings = $db->select('system_settings', ['setting_key', 'setting_value']);
|
||||
} catch (\Exception $e) {
|
||||
// 表不存在或其他错误,返回默认值
|
||||
self::$settings = self::getDefaults();
|
||||
return self::$settings;
|
||||
}
|
||||
$result = [];
|
||||
|
||||
foreach ($settings as $setting) {
|
||||
$result[$setting['setting_key']] = $setting['setting_value'];
|
||||
}
|
||||
|
||||
// 合并默认值
|
||||
$defaults = self::getDefaults();
|
||||
foreach ($defaults as $key => $value) {
|
||||
if (!isset($result[$key])) {
|
||||
$result[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
self::$settings = $result;
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
// 出错时返回默认值
|
||||
return self::getDefaults();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个设置值
|
||||
*/
|
||||
public static function get($key, $default = '') {
|
||||
$settings = self::getAll();
|
||||
return isset($settings[$key]) ? $settings[$key] : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认设置值
|
||||
*/
|
||||
private static function getDefaults() {
|
||||
return [
|
||||
'site_title' => 'PK10 Speed Racing',
|
||||
'site_description' => 'PK10 Speed Racing - Online Betting Platform',
|
||||
'site_keywords' => 'pk10, speed racing, betting, online game',
|
||||
'site_logo' => '/Static/images/logo.png',
|
||||
'site_favicon' => '/Static/css/favicon.ico',
|
||||
'site_copyright' => '© 2025 PK10 Racing. All rights reserved.'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存(当设置更新后调用)
|
||||
*/
|
||||
public static function clearCache() {
|
||||
self::$settings = null;
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class WebBaseController extends BaseController {
|
||||
|
||||
protected function render($viewPath, $data = []) {
|
||||
// 如果是绝对路径,直接使用
|
||||
if (strpos($viewPath, '/') === 0 || preg_match('/^[a-zA-Z]:\\\\/', $viewPath)) {
|
||||
$fullPath = $viewPath;
|
||||
} else {
|
||||
// 相对路径,按默认视图目录拼接
|
||||
$viewsDir = __DIR__ . '/../views/';
|
||||
$fullPath = $viewsDir . $viewPath;
|
||||
}
|
||||
|
||||
if (!file_exists($fullPath)) {
|
||||
throw new Exception("视图文件不存在: $fullPath");
|
||||
}
|
||||
|
||||
extract($data);
|
||||
|
||||
if (
|
||||
!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'
|
||||
) {
|
||||
include $fullPath;
|
||||
} else {
|
||||
ob_start();
|
||||
include $fullPath;
|
||||
$Content = ob_get_clean();
|
||||
|
||||
include __DIR__ . '/../views/Web/index.php'; // 主后台模板
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查前台用户登录状态
|
||||
* 如果未登录,重定向到登录页面
|
||||
*/
|
||||
protected function checkWebLogin() {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$timeout = isset($_SESSION['web_remember']) && $_SESSION['web_remember'] ? 604800 : 7200; // 记住我:7天,否则2小时
|
||||
|
||||
// 检查是否有用户ID
|
||||
if (!isset($_SESSION['web_user_id'])) {
|
||||
$this->redirectToLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取 IP 前三段(IPv4)
|
||||
function get_ip_prefix($ip, $segments = 3) {
|
||||
if (empty($ip)) return '';
|
||||
$parts = explode('.', $ip);
|
||||
if (count($parts) < 4) return $ip; // IPv6 或其他格式,直接返回
|
||||
return implode('.', array_slice($parts, 0, $segments));
|
||||
}
|
||||
|
||||
// 宽松 IP 检查(只比对前三段,例如 192.168.1.xxx)
|
||||
$currentIp = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
$sessionIp = $_SESSION['web_ip'] ?? '';
|
||||
|
||||
if (!empty($currentIp) && !empty($sessionIp)) {
|
||||
$current_ip_prefix = get_ip_prefix($currentIp, 3);
|
||||
$session_ip_prefix = get_ip_prefix($sessionIp, 3);
|
||||
|
||||
if ($current_ip_prefix !== $session_ip_prefix) {
|
||||
session_destroy();
|
||||
$this->redirectToLogin();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// User Agent 检查
|
||||
$currentUa = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
$sessionUa = $_SESSION['web_ua'] ?? '';
|
||||
|
||||
if (!empty($currentUa) && !empty($sessionUa) && $currentUa !== $sessionUa) {
|
||||
session_destroy();
|
||||
$this->redirectToLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
// 会话超时检查
|
||||
$lastActivity = $_SESSION['web_last_activity'] ?? 0;
|
||||
if (time() - $lastActivity > $timeout) {
|
||||
session_destroy();
|
||||
$this->redirectToLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新最后活动时间
|
||||
$_SESSION['web_last_activity'] = time();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重定向到登录页面
|
||||
*/
|
||||
private function redirectToLogin() {
|
||||
$currentUrl = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
$loginUrl = '/login?redirect=' . urlencode($currentUrl);
|
||||
|
||||
if (
|
||||
!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'
|
||||
) {
|
||||
// AJAX 请求,返回 JSON
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'redirect' => $loginUrl,
|
||||
'message' => 'Phiên đăng nhập đã hết hạn, vui lòng đăng nhập lại!'
|
||||
]);
|
||||
} else {
|
||||
// 普通请求,重定向
|
||||
header('Location: ' . $loginUrl);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户ID
|
||||
*/
|
||||
protected function getCurrentUserId() {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
return $_SESSION['web_user_id'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户名
|
||||
*/
|
||||
protected function getCurrentUsername() {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
return $_SESSION['web_username'] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
namespace App\Core;
|
||||
|
||||
class XocDiaAlgorithm implements GameAlgorithmInterface {
|
||||
|
||||
public static function generateResult(): array {
|
||||
$coins = [];
|
||||
for ($i = 0; $i < 4; $i++) $coins[] = (rand(0, 1) === 0) ? 'red' : 'white';
|
||||
return $coins;
|
||||
}
|
||||
|
||||
public static function generateControlledResult(array $bets, array $waterConfig, int $attempts = 100): array {
|
||||
if (empty($bets) || empty($waterConfig)) return self::generateResult();
|
||||
$bestResult = null;
|
||||
$bestProfit = PHP_INT_MIN;
|
||||
for ($i = 0; $i < $attempts; $i++) {
|
||||
$result = self::generateResult();
|
||||
$profit = self::calculatePlatformProfit($result, $bets);
|
||||
if ($profit > $bestProfit) { $bestProfit = $profit; $bestResult = $result; }
|
||||
}
|
||||
return $bestResult;
|
||||
}
|
||||
|
||||
public static function checkWin(array $result, string $betType, string $betTarget): bool {
|
||||
$redCount = count(array_filter($result, fn($c) => $c === 'red'));
|
||||
switch ($betTarget) {
|
||||
case 'even': return ($redCount % 2 === 0);
|
||||
case 'odd': return ($redCount % 2 === 1);
|
||||
case '4red': return ($redCount === 4);
|
||||
case '4white': return ($redCount === 0);
|
||||
case '3red1white': return ($redCount === 3);
|
||||
case '1red3white': return ($redCount === 1);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function calculatePlatformProfit(array $result, array $bets): float {
|
||||
$totalBet = 0; $totalPayout = 0;
|
||||
foreach ($bets as $bet) {
|
||||
$amount = (float)$bet['amount'];
|
||||
$totalBet += $amount;
|
||||
if (self::checkWin($result, $bet['bet_type'], $bet['bet_target'] ?? $bet['bet_value'] ?? '')) {
|
||||
$totalPayout += $amount + $amount * (float)$bet['odds'];
|
||||
}
|
||||
}
|
||||
return $totalBet - $totalPayout;
|
||||
}
|
||||
|
||||
public static function getResultTable(): ?string { return null; }
|
||||
|
||||
public static function formatResultForStorage(array $result, int $periodId): array {
|
||||
$redCount = count(array_filter($result, fn($c) => $c === 'red'));
|
||||
return [
|
||||
'dice1' => ($result[0] === 'red') ? 1 : 0,
|
||||
'dice2' => ($result[1] === 'red') ? 1 : 0,
|
||||
'dice3' => ($result[2] === 'red') ? 1 : 0,
|
||||
'total' => $redCount,
|
||||
];
|
||||
}
|
||||
|
||||
public static function parseResultFromDb(array $row): array {
|
||||
return [
|
||||
((int)($row['dice1'] ?? 0)) ? 'red' : 'white',
|
||||
((int)($row['dice2'] ?? 0)) ? 'red' : 'white',
|
||||
((int)($row['dice3'] ?? 0)) ? 'red' : 'white',
|
||||
// 第4个硬币从 result JSON 恢复,或根据 total 推断
|
||||
'white', // fallback
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user