Initial commit: 投注游戏平台初始化

This commit is contained in:
li
2026-02-25 01:26:58 +08:00
commit 77ca2cc8b3
275 changed files with 237479 additions and 0 deletions
+34
View File
@@ -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;
}
}
}
+113
View File
@@ -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;
}
}
+59
View File
@@ -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)];
}
}
+12
View File
@@ -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;
}
+20
View File
@@ -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;
}
}
+82
View File
@@ -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); }
+101
View File
@@ -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'];
}
}
+181
View File
@@ -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;
}
}
+20
View File
@@ -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);
}
}
+682
View File
@@ -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;
}
}
+226
View File
@@ -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) . '$/';
}
}
+79
View File
@@ -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;
}
}
+142
View File
@@ -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;
}
}
+70
View File
@@ -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
];
}
}