true, 'cookie_samesite' => 'Lax', 'cookie_secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'), // HTTPS 下仅安全传输 ]); } // 2. 自动加载(Core / App\Controllers / App\Models) spl_autoload_register(function ($class) { $prefixes = [ 'Core\\' => BASE_PATH . '/app/Core/', 'App\\Controllers\\' => BASE_PATH . '/app/Controllers/', 'App\\Models\\' => BASE_PATH . '/app/Models/', ]; foreach ($prefixes as $prefix => $base) { if (strncmp($class, $prefix, strlen($prefix)) === 0) { $rel = substr($class, strlen($prefix)); $file = $base . str_replace('\\', '/', $rel) . '.php'; if (is_file($file)) { require $file; return true; } } } return false; }); } public static function run() { self::init(); self::dispatch(self::parseRoute()); } /** 解析请求路径为段数组 */ public static function parseRoute(): array { $uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; // 仅当 SCRIPT_NAME 真实以 index.php 结尾时(Apache/生产模式)才剥离目录前缀; // 内置服务器路由模式下 SCRIPT_NAME 等于请求路径,此时不剥离,避免误删 admin 等段。 $script = $_SERVER['SCRIPT_NAME'] ?? '/index.php'; if (substr($script, -strlen('/index.php')) === '/index.php') { $dir = dirname($script); if ($dir !== '/' && strpos($uri, $dir) === 0) { $uri = substr($uri, strlen($dir)); } } $uri = preg_replace('#/index\.php$#i', '', $uri); $uri = trim($uri, '/'); if ($uri === '') return []; return explode('/', $uri); } /** 路由分发 */ public static function dispatch(array $segments) { // 静态资源直出 $asset = BASE_PATH . '/public/' . implode('/', $segments); if ($segments && is_file($asset) && !is_dir($asset)) { self::serveFile($asset); return; } $admin = self::config('app.admin_path', 'admin'); if (!empty($segments) && $segments[0] === $admin) { self::dispatchAdmin(array_slice($segments, 1)); return; } // 分系统入口:CRM 客户管理 / PSI 进销存(统一受 super_admin 与分系统权限管辖) // 大小写不敏感:/crm、/CRM、/psi、/PSI 均可进入,避免因 URL 大小写不同导致 404 if (!empty($segments)) { $seg0 = strtoupper($segments[0]); if ($seg0 === 'CRM' || $seg0 === 'PSI') { self::dispatchSubsys($seg0, array_slice($segments, 1)); return; } } self::dispatchFront($segments); } private static function dispatchSubsys(string $raw, array $s) { $sys = strtoupper($raw); // CRM / PSI $sysKey = $sys === 'PSI' ? 'psi' : 'crm'; // 未登录或无该系统角色:拦截(拥有 crm/psi 任意角色即可进入;写操作由各 Controller 的 subsys_admin 二次把关) if (!subsys_can_enter($sysKey)) { self::forbidden('您没有访问「' . $sys . '」系统的权限'); return; } // 统一入口:CRM\DashboardController / PSI\DashboardController 内部再做子路由 $ctrl = 'App\\Controllers\\' . $sys . '\\DashboardController'; $action = 'dispatch'; if (!class_exists($ctrl)) { self::notFound("系统不存在: $sys"); return; } $instance = new $ctrl(); if (!method_exists($instance, $action)) { self::notFound("入口不存在: $action"); return; } try { $html = call_user_func_array([$instance, $action], [$s]); } catch (\Throwable $e) { // 子系统异常兜底:保留左导 + 右框,框内显示错误,绝不白屏 error_log('Subsys[' . $sys . '] error: ' . $e->getMessage()); $html = self::subsysErrorFrame($sysKey, $instance, $e); } if (is_string($html)) echo $html; } private static function dispatchFront(array $s) { $key = $s[0] ?? ''; $seg2 = $s[1] ?? null; // 验证码图片输出(独立端点,直接输出 PNG) if ($key === 'captcha' && $seg2 === 'image') { captcha_image(); return; } // Sitemap 动态生成(XML 格式,搜索引擎自动抓取) if ($key === 'sitemap.xml') { self::outputSitemap(); return; } // 资源详情路由(带第二段 slug) if (($key === 'product' || $key === 'products') && $seg2) { self::call('App\\Controllers\\ProductController', 'show', [$seg2]); return; } if ($key === 'news' && $seg2) { self::call('App\\Controllers\\NewsController', 'show', [$seg2]); return; } if ($key === 'cases' && $seg2) { self::call('App\\Controllers\\CaseController', 'show', [$seg2]); return; } // 订单 / 支付(含网关异步通知,无需登录) if ($key === 'order') { $action = $s[1] ?? 'checkout'; $param = $s[2] ?? null; self::call('App\\Controllers\\OrderController', $action, [$param]); return; } if ($key === 'pay') { $action = $s[1] ?? 'notify'; $param = $s[2] ?? null; self::call('App\\Controllers\\PayController', $action, [$param]); return; } $map = [ '' => ['HomeController', 'index'], 'home' => ['HomeController', 'index'], 'products' => ['ProductController', 'index'], 'news' => ['NewsController', 'index'], 'cases' => ['CaseController', 'index'], 'page' => ['PageController', 'show'], 'contact' => ['ContactController', 'index'], ]; if (isset($map[$key])) { [$ctrl, $action] = $map[$key]; $param = $seg2; self::call('App\\Controllers\\' . $ctrl, $action, [$param]); return; } // 未知路径:按单页 slug 处理(/about、/service ...) self::call('App\\Controllers\\PageController', 'show', [$key]); } private static function dispatchAdmin(array $s) { $res = $s[0] ?? ''; $action = $s[1] ?? 'index'; $id = $s[2] ?? null; $map = [ '' => ['Admin\\DashboardController', 'index'], 'dashboard' => ['Admin\\DashboardController', 'index'], 'login' => ['Admin\\AuthController', 'login'], 'logout' => ['Admin\\AuthController', 'logout'], 'password' => ['Admin\\AuthController', 'password'], 'products' => ['Admin\\ProductController', 'index'], 'categories'=> ['Admin\\CategoryController', 'index'], 'news' => ['Admin\\NewsController', 'index'], 'pages' => ['Admin\\PageController', 'index'], 'banners' => ['Admin\\BannerController', 'index'], 'settings' => ['Admin\\SettingController', 'index'], 'theme' => ['Admin\\SettingController', 'theme'], 'seo' => ['Admin\\SettingController', 'seo'], 'users' => ['Admin\\UserController', 'index'], 'system' => ['Admin\\SystemController', 'index'], 'orders' => ['Admin\\OrderController', 'index'], 'payments' => ['Admin\\SettingController', 'payment'], 'cases' => ['Admin\\CaseController', 'index'], 'media' => ['Admin\\MediaController', 'index'], 'upgrade' => ['Admin\\UpgradeController', 'index'], 'db' => ['Admin\\DatabaseController', 'index'], ]; if (!isset($map[$res])) { self::notFound(); return; } // 权限能力检查:未登录或权限不足直接拦截(登录态由对应控制器再兜底) $capMap = [ 'users' => 'users', 'system' => 'users', 'upgrade' => 'users', 'db' => 'users', 'settings' => 'settings', 'theme' => 'settings', 'seo' => 'settings', 'payments' => 'settings', 'products' => 'products', 'categories'=> 'categories', 'news' => 'news', 'pages' => 'pages', 'banners' => 'banners', 'orders' => 'orders', 'cases' => 'cases', 'media' => 'pages', ]; if (isset($capMap[$res]) && !admin_can($capMap[$res])) { self::forbidden('当前账号无访问「' . $res . '」的权限'); return; } [$ctrl, $default] = $map[$res]; $method = ($action === 'index') ? $default : $action; self::call('App\\Controllers\\' . $ctrl, $method, [$id]); } private static function call($class, $method, array $args = []) { if (!class_exists($class)) { self::notFound("类不存在: $class"); return; } $instance = new $class(); if (!method_exists($instance, $method)) { self::notFound("方法不存在: $method"); return; } $html = call_user_func_array([$instance, $method], $args); if (is_string($html)) echo $html; } public static function notFound($msg = '') { http_response_code(404); echo '404

404

页面不存在' . ($msg ? ':' . htmlspecialchars($msg) : '') . '

返回首页

'; } public static function forbidden(string $msg = '') { http_response_code(403); echo '403

403

无访问权限' . ($msg ? ':' . htmlspecialchars($msg) : '') . '

返回后台

'; } /** * 子系统异常兜底框:左导 + 右框保持完整,框内显示错误信息,绝不白屏。 */ private static function subsysErrorFrame(string $sysKey, $instance, \Throwable $e): string { $nav = method_exists($instance, 'nav') ? $instance->nav('') : []; $content = '

页面加载出错

' . '

系统遇到问题,错误已记录,请稍后重试或联系管理员

' . '
' . e('错误信息:' . $e->getMessage()) . '
'; return \Core\View::make('layouts/subsys', [ 'content' => $content, '_sys' => $sysKey, '_nav' => $nav, '_seg' => '', ]); } private static function serveFile($file) { $real = realpath($file); $pub = realpath(BASE_PATH . '/public'); if ($real === false || $pub === false || strpos($real, $pub . DIRECTORY_SEPARATOR) !== 0 || !is_file($real)) { self::notFound('非法文件访问'); return; } $mime = mime_content_type($real); header('Content-Type: ' . $mime); header('Content-Length: ' . filesize($real)); readfile($real); exit; } /** 读取配置 config('app.name') */ /** * 生成站内 URL(兼容子目录部署)。 * 用法:App::url('PSI/reminders') -> https://host/base/PSI/reminders */ public static function url(string $path = ''): string { return site_url($path); } public static function config(string $key, $default = null) { if (empty(self::$config)) { self::$config = require BASE_PATH . '/config/config.php'; } $keys = explode('.', $key); $v = self::$config; foreach ($keys as $k) { if (!is_array($v) || !array_key_exists($k, $v)) return $default; $v = $v[$k]; } return $v; } /** 动态生成 Sitemap XML(Google/Bing/Baidu 自动抓取) */ private static function outputSitemap(): void { header('Content-Type: application/xml; charset=utf-8'); echo '' . "\n"; echo '' . "\n"; // 首页 echo '' . e(site_url()) . '1.0daily' . "\n"; // 静态页面 $staticPages = [ ['url' => site_url('products'), 'prio' => '0.9', 'freq' => 'weekly'], ['url' => site_url('news'), 'prio' => '0.8', 'freq' => 'daily'], ['url' => site_url('cases'), 'prio' => '0.8', 'freq' => 'weekly'], ['url' => site_url('page/about'), 'prio' => '0.7', 'freq' => 'monthly'], ['url' => site_url('contact'), 'prio' => '0.7', 'freq' => 'monthly'], ]; foreach ($staticPages as $p) { echo '' . e($p['url']) . '' . $p['prio'] . '' . $p['freq'] . '' . "\n"; } // 动态页面列表:产品 / 新闻 / 案例 / 单页 $models = [ ['class' => 'App\\Models\\Product', 'method' => 'all', 'urlPrefix' => 'product/', 'prio' => '0.85', 'freq' => 'weekly'], ['class' => 'App\\Models\\News', 'method' => 'published', 'urlPrefix' => 'news/', 'prio' => '0.75', 'freq' => 'weekly'], ['class' => 'App\\Models\\CustomerCase', 'method' => 'published', 'urlPrefix' => 'cases/', 'prio' => '0.75', 'freq' => 'weekly'], ['class' => 'App\\Models\\Page', 'method' => 'all', 'urlPrefix' => 'page/', 'prio' => '0.6', 'freq' => 'monthly'], ]; foreach ($models as $m) { if (!class_exists($m['class'])) continue; try { $instance = new $m['class'](); $items = []; if ($m['method'] === 'all') { $items = $instance->all(); } elseif ($m['method'] === 'published') { $items = $instance->published(200); } foreach ($items as $item) { $slug = $item['slug'] ?? ($item['id'] ?? ''); if (empty($slug)) continue; $url = site_url($m['urlPrefix'] . $slug); $lastmod = ''; if (!empty($item['updated_at'])) { $lastmod = '' . e($item['updated_at']) . ''; } elseif (!empty($item['created_at'])) { $lastmod = '' . e($item['created_at']) . ''; } echo '' . e($url) . '' . $lastmod . '' . $m['prio'] . '' . $m['freq'] . '' . "\n"; } } catch (\Throwable $e) { // 静默跳过异常的模型,保证 sitemap 完整性 continue; } } // 产品分类页(/products?cat=ID) if (class_exists('App\\Models\\Category')) { try { $catM = new \App\Models\Category(); foreach ($catM->all() as $c) { $cid = $c['id'] ?? 0; if (!$cid) continue; $url = site_url('products?cat=' . $cid); echo '' . e($url) . '0.7weekly' . "\n"; } } catch (\Throwable $e) { // 忽略 } } echo ''; exit; } }