文件还在测试中

This commit is contained in:
2026-08-08 15:53:53 +08:00
parent 5f1e7adb64
commit 8101177cb5
241 changed files with 18074 additions and 22 deletions
+407
View File
@@ -0,0 +1,407 @@
<?php
namespace Core;
/**
* 应用核心:自动加载、配置、路由、分发
*/
class App
{
public static $config = [];
/** 初始化:辅助函数、配置、会话、自动加载(install 脚本可复用) */
public static function init()
{
// 0. 全局辅助函数
require_once BASE_PATH . '/app/Core/Helper.php';
// 1. 时区 & 会话
date_default_timezone_set(self::config('app.timezone', 'Asia/Shanghai'));
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start([
'cookie_httponly' => 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 '<!doctype html><meta charset=utf-8><title>404</title>
<style>body{font-family:system-ui;display:grid;place-items:center;height:100vh;margin:0;background:#0f172a;color:#e2e8f0}
.b{text-align:center}.c{color:#38bdf8;font-size:64px;font-weight:800;margin:0}</style>
<div class="b"><p class="c">404</p><p>页面不存在' . ($msg ? '' . htmlspecialchars($msg) : '') . '</p>
<p><a style="color:#38bdf8" href="' . site_url() . '">返回首页</a></p></div>';
}
public static function forbidden(string $msg = '')
{
http_response_code(403);
echo '<!doctype html><meta charset=utf-8><title>403</title>
<style>body{font-family:system-ui;display:grid;place-items:center;height:100vh;margin:0;background:#0f172a;color:#e2e8f0}
.b{text-align:center}.c{color:#f87171;font-size:56px;font-weight:800;margin:0}</style>
<div class="b"><p class="c">403</p><p>无访问权限' . ($msg ? '' . htmlspecialchars($msg) : '') . '</p>
<p><a style="color:#38bdf8" href="' . site_url('admin') . '">返回后台</a></p></div>';
}
/**
* 子系统异常兜底框:左导 + 右框保持完整,框内显示错误信息,绝不白屏。
*/
private static function subsysErrorFrame(string $sysKey, $instance, \Throwable $e): string
{
$nav = method_exists($instance, 'nav') ? $instance->nav('') : [];
$content = '<div class="page-head"><h1>页面加载出错</h1>'
. '<p class="sub">系统遇到问题,错误已记录,请稍后重试或联系管理员</p></div>'
. '<div class="alert alert-err">' . e('错误信息:' . $e->getMessage()) . '</div>';
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 XMLGoogle/Bing/Baidu 自动抓取) */
private static function outputSitemap(): void
{
header('Content-Type: application/xml; charset=utf-8');
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
// 首页
echo '<url><loc>' . e(site_url()) . '</loc><priority>1.0</priority><changefreq>daily</changefreq></url>' . "\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 '<url><loc>' . e($p['url']) . '</loc><priority>' . $p['prio'] . '</priority><changefreq>' . $p['freq'] . '</changefreq></url>' . "\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 = '<lastmod>' . e($item['updated_at']) . '</lastmod>';
} elseif (!empty($item['created_at'])) {
$lastmod = '<lastmod>' . e($item['created_at']) . '</lastmod>';
}
echo '<url><loc>' . e($url) . '</loc>' . $lastmod . '<priority>' . $m['prio'] . '</priority><changefreq>' . $m['freq'] . '</changefreq></url>' . "\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 '<url><loc>' . e($url) . '</loc><priority>0.7</priority><changefreq>weekly</changefreq></url>' . "\n";
}
} catch (\Throwable $e) {
// 忽略
}
}
echo '</urlset>';
exit;
}
}