- 拆分 HomeController → TransactionController, ReportWebController, FollowPlanController - 新增 Service 层: TransactionService, ReportService, FollowPlanService - pk10.php JS 抽离为 4 个独立文件 (sound/race/bet/poll) - 前台新增报表查询页面 (/report) + 跟单计划页面 (/follow-plan) - 后台新增跟单计划管理 + 用户输赢明细统计 - 封盘状态显示倒计时 (x:xx) - 音效仅在开奖弹窗打开时播放 - 路由按模块分组整理 - autoload 支持 App\Services 命名空间
542 lines
19 KiB
PHP
542 lines
19 KiB
PHP
<?php
|
|
/**
|
|
* 重构验证测试脚本
|
|
* 覆盖:语法检查、路由完整性、Service层、Controller调用链、JS文件、JS全局变量、View文件、i18n、路由无死链
|
|
*/
|
|
|
|
$ROOT = dirname(__DIR__);
|
|
$pass = 0;
|
|
$fail = 0;
|
|
$results = [];
|
|
|
|
function test_pass($desc) {
|
|
global $pass, $results;
|
|
$pass++;
|
|
$results[] = "[PASS] $desc";
|
|
}
|
|
function test_fail($desc, $reason) {
|
|
global $fail, $results;
|
|
$fail++;
|
|
$results[] = "[FAIL] $desc - $reason";
|
|
}
|
|
|
|
// ============================================================
|
|
// 1. PHP 语法检查
|
|
// ============================================================
|
|
echo "=== 1. PHP Syntax Check ===\n";
|
|
$phpFiles = [];
|
|
$dirs = ['App', 'Db', 'Lang', 'Models', 'Core', 'cron'];
|
|
foreach ($dirs as $dir) {
|
|
$path = $ROOT . '/' . $dir;
|
|
if (!is_dir($path)) continue;
|
|
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path));
|
|
foreach ($it as $file) {
|
|
if ($file->isFile() && $file->getExtension() === 'php') {
|
|
$phpFiles[] = $file->getPathname();
|
|
}
|
|
}
|
|
}
|
|
// Also check root-level PHP files
|
|
foreach (glob($ROOT . '/*.php') as $f) {
|
|
$phpFiles[] = $f;
|
|
}
|
|
|
|
$syntaxErrors = [];
|
|
foreach ($phpFiles as $file) {
|
|
$output = [];
|
|
$ret = 0;
|
|
exec('php -l ' . escapeshellarg($file) . ' 2>&1', $output, $ret);
|
|
if ($ret !== 0) {
|
|
$relPath = str_replace($ROOT . '/', '', $file);
|
|
$syntaxErrors[] = $relPath . ': ' . implode(' ', $output);
|
|
}
|
|
}
|
|
|
|
if (empty($syntaxErrors)) {
|
|
test_pass("PHP syntax check: all " . count($phpFiles) . " files pass");
|
|
} else {
|
|
foreach ($syntaxErrors as $err) {
|
|
test_fail("PHP syntax error", $err);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// 2. 路由完整性验证
|
|
// ============================================================
|
|
echo "=== 2. Route Integrity ===\n";
|
|
$routes = include $ROOT . '/routes.php';
|
|
$seenPaths = [];
|
|
$routeIssues = 0;
|
|
|
|
foreach ($routes as $idx => $route) {
|
|
$method = $route[0];
|
|
$path = $route[1];
|
|
$action = $route[2];
|
|
|
|
// 检查重复路由 path (method+path)
|
|
$key = $method . ':' . $path;
|
|
if (isset($seenPaths[$key])) {
|
|
test_fail("Route duplicate", "'{$key}' duplicated at line " . ($idx + 1));
|
|
$routeIssues++;
|
|
}
|
|
$seenPaths[$key] = true;
|
|
|
|
// 解析 Controller@method
|
|
if (strpos($action, '@') === false) {
|
|
test_fail("Route format", "'{$path}' action '{$action}' missing @method");
|
|
$routeIssues++;
|
|
continue;
|
|
}
|
|
list($controllerClass, $methodName) = explode('@', $action);
|
|
|
|
// 转换类名到文件路径
|
|
$controllerFile = $ROOT . '/' . str_replace('\\', '/', ltrim($controllerClass, '\\')) . '.php';
|
|
if (!file_exists($controllerFile)) {
|
|
test_fail("Route controller missing", "'{$path}' -> {$controllerClass} file not found: {$controllerFile}");
|
|
$routeIssues++;
|
|
continue;
|
|
}
|
|
|
|
// 检查方法是否存在(通过正则搜索)
|
|
$content = file_get_contents($controllerFile);
|
|
if (!preg_match('/function\s+' . preg_quote($methodName) . '\s*\(/', $content)) {
|
|
test_fail("Route method missing", "'{$path}' -> {$controllerClass}@{$methodName} method not found");
|
|
$routeIssues++;
|
|
}
|
|
}
|
|
|
|
if ($routeIssues === 0) {
|
|
test_pass("Route integrity: all " . count($routes) . " routes valid");
|
|
}
|
|
|
|
// ============================================================
|
|
// 3. Service 层验证
|
|
// ============================================================
|
|
echo "=== 3. Service Layer ===\n";
|
|
|
|
// TransactionService 方法检查
|
|
$tsFile = $ROOT . '/App/Services/TransactionService.php';
|
|
if (!file_exists($tsFile)) {
|
|
test_fail("TransactionService", "File not found");
|
|
} else {
|
|
$tsContent = file_get_contents($tsFile);
|
|
$tsMethods = ['transfer', 'fundRequest'];
|
|
foreach ($tsMethods as $m) {
|
|
if (preg_match('/function\s+' . $m . '\s*\(/', $tsContent)) {
|
|
test_pass("TransactionService has {$m}() method");
|
|
} else {
|
|
test_fail("TransactionService", "Missing method {$m}()");
|
|
}
|
|
}
|
|
}
|
|
|
|
// ReportService 方法检查
|
|
$rsFile = $ROOT . '/App/Services/ReportService.php';
|
|
if (!file_exists($rsFile)) {
|
|
test_fail("ReportService", "File not found");
|
|
} else {
|
|
$rsContent = file_get_contents($rsFile);
|
|
$rsMethods = ['getUserReport', 'getOverviewStats', 'getDailyStats', 'getUserWinLoss', 'getTopUsers', 'getAgentStats'];
|
|
foreach ($rsMethods as $m) {
|
|
if (preg_match('/function\s+' . $m . '\s*\(/', $rsContent)) {
|
|
test_pass("ReportService has {$m}() method");
|
|
} else {
|
|
test_fail("ReportService", "Missing method {$m}()");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Service 纯业务逻辑检查(不含 HTTP 操作)
|
|
$serviceFiles = glob($ROOT . '/App/Services/*.php');
|
|
$forbiddenPatterns = [
|
|
'header\s*\(' => 'header()',
|
|
'\becho\b' => 'echo',
|
|
'\$_GET' => '$_GET',
|
|
'\$_POST' => '$_POST',
|
|
'\$_SESSION' => '$_SESSION',
|
|
];
|
|
foreach ($serviceFiles as $sf) {
|
|
$sfContent = file_get_contents($sf);
|
|
$sfName = basename($sf);
|
|
$sfClean = true;
|
|
foreach ($forbiddenPatterns as $pattern => $label) {
|
|
if (preg_match('/' . $pattern . '/', $sfContent)) {
|
|
test_fail("Service purity: {$sfName}", "Contains forbidden pattern: {$label}");
|
|
$sfClean = false;
|
|
}
|
|
}
|
|
if ($sfClean) {
|
|
test_pass("Service purity: {$sfName} is clean (no HTTP operations)");
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// 4. Controller → Service 调用链验证
|
|
// ============================================================
|
|
echo "=== 4. Controller-Service Call Chain ===\n";
|
|
|
|
// TransactionController must call TransactionService
|
|
$tcFile = $ROOT . '/App/Controllers/Web/TransactionController.php';
|
|
if (file_exists($tcFile)) {
|
|
$tcContent = file_get_contents($tcFile);
|
|
// fundRequest calls TransactionService
|
|
if (preg_match('/TransactionService/', $tcContent) && preg_match('/function\s+fundRequest/', $tcContent)) {
|
|
test_pass("TransactionController::fundRequest calls TransactionService");
|
|
} else {
|
|
test_fail("TransactionController", "fundRequest does not call TransactionService");
|
|
}
|
|
// transfer calls TransactionService
|
|
if (preg_match('/TransactionService/', $tcContent) && preg_match('/function\s+transfer/', $tcContent)) {
|
|
test_pass("TransactionController::transfer calls TransactionService");
|
|
} else {
|
|
test_fail("TransactionController", "transfer does not call TransactionService");
|
|
}
|
|
} else {
|
|
test_fail("TransactionController", "File not found");
|
|
}
|
|
|
|
// ReportWebController must call ReportService
|
|
$rwcFile = $ROOT . '/App/Controllers/Web/ReportWebController.php';
|
|
if (file_exists($rwcFile)) {
|
|
$rwcContent = file_get_contents($rwcFile);
|
|
if (preg_match('/ReportService/', $rwcContent) && preg_match('/function\s+userReportApi/', $rwcContent)) {
|
|
test_pass("ReportWebController::userReportApi calls ReportService");
|
|
} else {
|
|
test_fail("ReportWebController", "userReportApi does not call ReportService");
|
|
}
|
|
} else {
|
|
test_fail("ReportWebController", "File not found");
|
|
}
|
|
|
|
// Admin\ReportController must call ReportService
|
|
$arcFile = $ROOT . '/App/Controllers/Admin/ReportController.php';
|
|
if (file_exists($arcFile)) {
|
|
$arcContent = file_get_contents($arcFile);
|
|
$arcOk = true;
|
|
if (!preg_match('/ReportService/', $arcContent)) {
|
|
test_fail("Admin\\ReportController", "Does not reference ReportService");
|
|
$arcOk = false;
|
|
}
|
|
if (preg_match('/function\s+index/', $arcContent) && preg_match('/ReportService/', $arcContent)) {
|
|
test_pass("Admin\\ReportController::index calls ReportService");
|
|
} else if ($arcOk) {
|
|
test_fail("Admin\\ReportController", "index does not call ReportService");
|
|
}
|
|
if (preg_match('/function\s+export/', $arcContent) && preg_match('/ReportService/', $arcContent)) {
|
|
test_pass("Admin\\ReportController::export calls ReportService");
|
|
} else if ($arcOk) {
|
|
test_fail("Admin\\ReportController", "export does not call ReportService");
|
|
}
|
|
} else {
|
|
test_fail("Admin\\ReportController", "File not found");
|
|
}
|
|
|
|
// HomeController must NOT contain old methods
|
|
$hcFile = $ROOT . '/App/Controllers/Web/HomeController.php';
|
|
if (file_exists($hcFile)) {
|
|
$hcContent = file_get_contents($hcFile);
|
|
$removedMethods = ['fundRequest', 'transfer', 'report', 'followPlan', 'userReportApi'];
|
|
$hcClean = true;
|
|
foreach ($removedMethods as $rm) {
|
|
if (preg_match('/function\s+' . $rm . '\s*\(/', $hcContent)) {
|
|
test_fail("HomeController cleanup", "Still contains method {$rm}()");
|
|
$hcClean = false;
|
|
}
|
|
}
|
|
if ($hcClean) {
|
|
test_pass("HomeController no longer contains removed methods (fundRequest/transfer/report/followPlan/userReportApi)");
|
|
}
|
|
} else {
|
|
test_fail("HomeController", "File not found");
|
|
}
|
|
|
|
// ============================================================
|
|
// 5. JS 文件验证
|
|
// ============================================================
|
|
echo "=== 5. JS File Validation ===\n";
|
|
|
|
$jsFiles = ['pk10-sound.js', 'pk10-race.js', 'pk10-bet.js', 'pk10-poll.js'];
|
|
foreach ($jsFiles as $jsf) {
|
|
$jsPath = $ROOT . '/Static/js/' . $jsf;
|
|
if (!file_exists($jsPath)) {
|
|
test_fail("JS file exists: {$jsf}", "File not found");
|
|
continue;
|
|
}
|
|
$jsSize = filesize($jsPath);
|
|
if ($jsSize === 0) {
|
|
test_fail("JS file non-empty: {$jsf}", "File is empty");
|
|
continue;
|
|
}
|
|
test_pass("JS file exists and non-empty: {$jsf} ({$jsSize} bytes)");
|
|
|
|
// No PHP tags
|
|
$jsContent = file_get_contents($jsPath);
|
|
if (preg_match('/<\?php|<\?=/', $jsContent)) {
|
|
test_fail("JS no PHP tags: {$jsf}", "Contains PHP tags");
|
|
} else {
|
|
test_pass("JS no PHP tags: {$jsf}");
|
|
}
|
|
}
|
|
|
|
// pk10.php checks
|
|
$pk10File = $ROOT . '/App/Views/Web/pk10.php';
|
|
if (file_exists($pk10File)) {
|
|
$pk10Content = file_get_contents($pk10File);
|
|
|
|
// window.PK10_CONFIG
|
|
if (strpos($pk10Content, 'PK10_CONFIG') !== false) {
|
|
test_pass("pk10.php contains PK10_CONFIG configuration block");
|
|
} else {
|
|
test_fail("pk10.php PK10_CONFIG", "Missing window.PK10_CONFIG block");
|
|
}
|
|
|
|
// 4 script src references
|
|
$scriptCount = 0;
|
|
foreach ($jsFiles as $jsf) {
|
|
if (strpos($pk10Content, $jsf) !== false) {
|
|
$scriptCount++;
|
|
}
|
|
}
|
|
if ($scriptCount === 4) {
|
|
test_pass("pk10.php includes all 4 pk10-*.js script references");
|
|
} else {
|
|
test_fail("pk10.php script references", "Found {$scriptCount}/4 JS file references");
|
|
}
|
|
|
|
// JS 引用顺序: sound → race → bet → poll
|
|
$positions = [];
|
|
foreach ($jsFiles as $jsf) {
|
|
$pos = strpos($pk10Content, $jsf);
|
|
if ($pos !== false) {
|
|
$positions[$jsf] = $pos;
|
|
}
|
|
}
|
|
if (count($positions) === 4) {
|
|
$ordered = array_keys($positions);
|
|
usort($ordered, function($a, $b) use ($positions) {
|
|
return $positions[$a] - $positions[$b];
|
|
});
|
|
$expected = ['pk10-sound.js', 'pk10-race.js', 'pk10-bet.js', 'pk10-poll.js'];
|
|
if ($ordered === $expected) {
|
|
test_pass("pk10.php JS include order correct: sound -> race -> bet -> poll");
|
|
} else {
|
|
test_fail("pk10.php JS order", "Expected: " . implode(' -> ', $expected) . " Got: " . implode(' -> ', $ordered));
|
|
}
|
|
}
|
|
} else {
|
|
test_fail("pk10.php", "File not found");
|
|
}
|
|
|
|
// ============================================================
|
|
// 6. JS 全局变量/函数完整性
|
|
// ============================================================
|
|
echo "=== 6. JS Global Function Integrity ===\n";
|
|
|
|
$jsSoundContent = file_exists($ROOT . '/Static/js/pk10-sound.js') ? file_get_contents($ROOT . '/Static/js/pk10-sound.js') : '';
|
|
$jsRaceContent = file_exists($ROOT . '/Static/js/pk10-race.js') ? file_get_contents($ROOT . '/Static/js/pk10-race.js') : '';
|
|
$jsBetContent = file_exists($ROOT . '/Static/js/pk10-bet.js') ? file_get_contents($ROOT . '/Static/js/pk10-bet.js') : '';
|
|
$jsPollContent = file_exists($ROOT . '/Static/js/pk10-poll.js') ? file_get_contents($ROOT . '/Static/js/pk10-poll.js') : '';
|
|
|
|
// SoundFX in pk10-sound.js
|
|
if (preg_match('/\bSoundFX\b/', $jsSoundContent)) {
|
|
test_pass("SoundFX defined in pk10-sound.js");
|
|
} else {
|
|
test_fail("JS global: SoundFX", "Not found in pk10-sound.js");
|
|
}
|
|
|
|
// Race functions in pk10-race.js
|
|
$raceFuncs = ['animateRace', 'startIdleAnimation', 'resetRace', 'updateSceneHeader'];
|
|
foreach ($raceFuncs as $rf) {
|
|
if (preg_match('/\b' . $rf . '\b/', $jsRaceContent)) {
|
|
test_pass("{$rf} defined in pk10-race.js");
|
|
} else {
|
|
test_fail("JS global: {$rf}", "Not found in pk10-race.js");
|
|
}
|
|
}
|
|
|
|
// Bet functions in pk10-bet.js
|
|
$betFuncs = ['renderMyBets', 'renderLastMyBets', 'formatBetLabel'];
|
|
foreach ($betFuncs as $bf) {
|
|
if (preg_match('/\b' . $bf . '\b/', $jsBetContent)) {
|
|
test_pass("{$bf} defined in pk10-bet.js");
|
|
} else {
|
|
test_fail("JS global: {$bf}", "Not found in pk10-bet.js");
|
|
}
|
|
}
|
|
|
|
// pollPeriod in pk10-poll.js
|
|
if (preg_match('/\bpollPeriod\b/', $jsPollContent)) {
|
|
test_pass("pollPeriod defined in pk10-poll.js");
|
|
} else {
|
|
test_fail("JS global: pollPeriod", "Not found in pk10-poll.js");
|
|
}
|
|
|
|
// ============================================================
|
|
// 7. View 文件验证
|
|
// ============================================================
|
|
echo "=== 7. View File Validation ===\n";
|
|
|
|
// report.php
|
|
$reportView = $ROOT . '/App/Views/Web/report.php';
|
|
if (file_exists($reportView)) {
|
|
$rvContent = file_get_contents($reportView);
|
|
test_pass("report.php exists");
|
|
if (strpos($rvContent, '/api/user-report') !== false) {
|
|
test_pass("report.php contains /api/user-report fetch call");
|
|
} else {
|
|
test_fail("report.php content", "Missing /api/user-report fetch call");
|
|
}
|
|
} else {
|
|
test_fail("report.php", "File not found");
|
|
}
|
|
|
|
// follow_plan.php
|
|
$fpView = $ROOT . '/App/Views/Web/follow_plan.php';
|
|
if (file_exists($fpView)) {
|
|
$fpContent = file_get_contents($fpView);
|
|
test_pass("follow_plan.php exists");
|
|
if (strpos($fpContent, 'PK10_CONFIG') !== false || strpos($fpContent, 'pk10') !== false || strpos($fpContent, 'follow') !== false) {
|
|
test_pass("follow_plan.php contains expected content");
|
|
} else {
|
|
test_fail("follow_plan.php content", "Missing expected PK10_CONFIG or related content");
|
|
}
|
|
} else {
|
|
test_fail("follow_plan.php", "File not found");
|
|
}
|
|
|
|
// profile.php
|
|
$profileView = $ROOT . '/App/Views/Web/profile.php';
|
|
if (file_exists($profileView)) {
|
|
$pvContent = file_get_contents($profileView);
|
|
$profileOk = true;
|
|
if (strpos($pvContent, '/report') !== false) {
|
|
test_pass("profile.php contains /report link");
|
|
} else {
|
|
test_fail("profile.php link", "Missing /report link");
|
|
$profileOk = false;
|
|
}
|
|
if (strpos($pvContent, '/follow-plan') !== false || strpos($pvContent, 'follow_plan') !== false || strpos($pvContent, 'follow-plan') !== false) {
|
|
test_pass("profile.php contains /follow-plan link");
|
|
} else {
|
|
test_fail("profile.php link", "Missing /follow-plan link");
|
|
}
|
|
} else {
|
|
test_fail("profile.php", "File not found");
|
|
}
|
|
|
|
// pk10.php localLockCountdown
|
|
if (file_exists($pk10File)) {
|
|
$pk10Content = $pk10Content ?? file_get_contents($pk10File);
|
|
if (strpos($pk10Content, 'localLockCountdown') !== false) {
|
|
test_pass("pk10.php contains localLockCountdown in statusBadge code");
|
|
} else {
|
|
test_fail("pk10.php localLockCountdown", "Missing localLockCountdown reference");
|
|
}
|
|
} else {
|
|
test_fail("pk10.php", "File not found");
|
|
}
|
|
|
|
// ============================================================
|
|
// 8. i18n 验证
|
|
// ============================================================
|
|
echo "=== 8. i18n Validation ===\n";
|
|
|
|
$zhFile = $ROOT . '/Lang/zh.php';
|
|
$enFile = $ROOT . '/Lang/en.php';
|
|
|
|
if (!file_exists($zhFile) || !file_exists($enFile)) {
|
|
test_fail("i18n files", "zh.php or en.php not found");
|
|
} else {
|
|
$zhLang = include $zhFile;
|
|
$enLang = include $enFile;
|
|
|
|
$requiredKeys = ['report_query', 'follow_plan', 'today', 'yesterday'];
|
|
foreach ($requiredKeys as $rk) {
|
|
if (isset($zhLang[$rk])) {
|
|
test_pass("zh.php has key '{$rk}'");
|
|
} else {
|
|
test_fail("zh.php i18n", "Missing key '{$rk}'");
|
|
}
|
|
if (isset($enLang[$rk])) {
|
|
test_pass("en.php has key '{$rk}'");
|
|
} else {
|
|
test_fail("en.php i18n", "Missing key '{$rk}'");
|
|
}
|
|
}
|
|
|
|
// Key count match
|
|
$zhCount = count($zhLang);
|
|
$enCount = count($enLang);
|
|
if ($zhCount === $enCount) {
|
|
test_pass("i18n key count match: zh={$zhCount}, en={$enCount}");
|
|
} else {
|
|
$diff = abs($zhCount - $enCount);
|
|
// Find missing keys
|
|
$missingInEn = array_diff(array_keys($zhLang), array_keys($enLang));
|
|
$missingInZh = array_diff(array_keys($enLang), array_keys($zhLang));
|
|
$detail = "zh={$zhCount}, en={$enCount} (diff={$diff})";
|
|
if (!empty($missingInEn)) {
|
|
$detail .= " | Missing in en: " . implode(', ', array_slice($missingInEn, 0, 10));
|
|
}
|
|
if (!empty($missingInZh)) {
|
|
$detail .= " | Missing in zh: " . implode(', ', array_slice($missingInZh, 0, 10));
|
|
}
|
|
test_fail("i18n key count mismatch", $detail);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// 9. 路由无死链验证
|
|
// ============================================================
|
|
echo "=== 9. Route Dead Link Check ===\n";
|
|
|
|
$deadLinks = 0;
|
|
foreach ($routes as $route) {
|
|
$action = $route[2];
|
|
if (strpos($action, '@') === false) continue;
|
|
list($controllerClass, $methodName) = explode('@', $action);
|
|
$controllerFile = $ROOT . '/' . str_replace('\\', '/', ltrim($controllerClass, '\\')) . '.php';
|
|
|
|
if (!file_exists($controllerFile)) {
|
|
// Already reported in section 2
|
|
$deadLinks++;
|
|
continue;
|
|
}
|
|
|
|
$content = file_get_contents($controllerFile);
|
|
if (!preg_match('/function\s+' . preg_quote($methodName) . '\s*\(/', $content)) {
|
|
// Already reported in section 2
|
|
$deadLinks++;
|
|
}
|
|
}
|
|
|
|
if ($deadLinks === 0) {
|
|
test_pass("No dead route links: all " . count($routes) . " routes point to existing methods");
|
|
} else {
|
|
test_fail("Dead route links", "{$deadLinks} routes point to non-existent controllers/methods (see section 2 for details)");
|
|
}
|
|
|
|
// ============================================================
|
|
// 汇总输出
|
|
// ============================================================
|
|
echo "\n" . str_repeat('=', 60) . "\n";
|
|
echo "TEST RESULTS\n";
|
|
echo str_repeat('=', 60) . "\n\n";
|
|
|
|
foreach ($results as $r) {
|
|
echo $r . "\n";
|
|
}
|
|
|
|
$total = $pass + $fail;
|
|
echo "\n" . str_repeat('-', 60) . "\n";
|
|
echo "Total: {$total} tests | Pass: {$pass} | Fail: {$fail}\n";
|
|
echo str_repeat('-', 60) . "\n";
|
|
|
|
if ($fail > 0) {
|
|
echo "\n*** {$fail} FAILURE(S) DETECTED ***\n";
|
|
exit(1);
|
|
} else {
|
|
echo "\n*** ALL TESTS PASSED ***\n";
|
|
exit(0);
|
|
}
|