> */ public static function all(bool $force = false): array { if (!$force && is_array($apps = sysvar(self::CACHE_APPS))) { return $apps; } $apps = array_merge(self::local($force), self::allPlugins($force)); ksort($apps); return sysvar(self::CACHE_APPS, $apps); } /** * 获取本地 app/* 应用定义. * * @return array> */ public static function local(bool $force = false): array { if (!$force && is_array($apps = sysvar(self::CACHE_LOCALS))) { return $apps; } $apps = self::discoverLocalApps(); ksort($apps); return sysvar(self::CACHE_LOCALS, $apps); } /** * 获取插件应用定义. * * @return array> */ public static function plugins(bool $force = false): array { return self::allPlugins(false, $force); } /** * 获取全部应用编号. * * @return string[] */ public static function codes(bool $force = false): array { return array_keys(self::all($force)); } /** * 获取默认本地应用编号. */ public static function defaultAppCode(): string { $apps = self::local(); $code = strval(Library::$sapp->config->get('route.default_app') ?: Library::$sapp->config->get('app.single_app') ?: ''); if ($code !== '' && !in_array($code, self::IGNORE_LOCAL_APPS, true) && isset($apps[$code])) { return $code; } if (isset($apps['index'])) { return 'index'; } return strval(array_key_first($apps) ?: 'index'); } /** * 获取默认本地应用编号. */ public static function singleCode(): string { return self::defaultAppCode(); } /** * 获取指定应用定义. * * @param ?string $code 应用编号 */ public static function get(?string $code = null, bool $force = false): ?array { $apps = self::all($force); return is_null($code) ? $apps : ($apps[$code] ?? null); } /** * 按首段路径命中本地应用. * * @return null|array */ public static function matchPath(string $pathinfo, bool $force = false): ?array { $pathinfo = trim($pathinfo, '\/'); if ($pathinfo === '') { return null; } [$prefix, $suffix] = array_pad(explode('/', $pathinfo, 2), 2, ''); if (strpos($prefix, '.')) { $prefix = strstr($prefix, '.', true) ?: $prefix; } if ($prefix === '' || !($app = self::localApp($prefix, $force))) { return null; } $app['matched_prefix'] = $prefix; $app['pathinfo'] = $suffix; return $app; } /** * 获取指定本地应用定义. */ public static function localApp(?string $code = null, bool $force = false): ?array { $apps = self::local($force); return is_null($code) ? null : ($apps[$code] ?? null); } /** * 判断插件是否存在。 * @param string $code 插件编码或别名 * @param bool $force 强制刷新 */ public static function pluginExists(string $code, bool $force = false): bool { return self::resolvePlugin($code, false, $force) !== null; } /** * 解析插件编码或别名。 * @param ?string $name 插件编码或别名 * @param bool $append 关联安装信息 * @param bool $force 强制刷新 */ public static function resolvePlugin(?string $name, bool $append = false, bool $force = false): ?array { if ($name === null || $name === '') { return null; } $plugins = self::allPlugins($append, $force); if (isset($plugins[$name])) { return $plugins[$name]; } $bind = self::pluginBindings($force)[$name] ?? null; if ($bind && isset($plugins[$bind])) { return $plugins[$bind]; } $code = self::pluginAliases($force)[$name] ?? null; return $code ? ($plugins[$code] ?? null) : null; } /** * 获取全部插件定义。 * @param bool $append 关联安装信息 * @param bool $force 强制刷新 */ public static function allPlugins(bool $append = false, bool $force = false): array { if (!$append && !$force && is_array($plugins = sysvar(self::CACHE_PLUGINS))) { return $plugins; } $plugins = []; foreach ((array)Plugin::get() as $code => $plugin) { $plugins[$code] = self::normalizePlugin($code, $plugin); } ksort($plugins); if (!$append) { sysvar(self::CACHE_PLUGINS, $plugins); return $plugins; } return array_map(static function (array $plugin): array { return self::appendPluginInstall($plugin); }, $plugins); } /** * 获取指定插件定义。 * @param ?string $code 插件编码 * @param bool $append 关联安装信息 * @param bool $force 强制刷新 */ public static function getPlugin(?string $code = null, bool $append = false, bool $force = false): ?array { $plugins = self::allPlugins($append, $force); return is_null($code) ? $plugins : ($plugins[$code] ?? null); } /** * 获取插件前缀绑定关系。 * @param bool $force 强制刷新 * @return array */ public static function pluginBindings(bool $force = false): array { if (!$force && is_array($bindings = sysvar(self::CACHE_BINDINGS))) { return $bindings; } $bindings = []; foreach (self::allPlugins(false, $force) as $code => $plugin) { foreach ($plugin['prefixes'] ?? [] as $prefix) { if ($prefix === '') { continue; } if (isset($bindings[$prefix]) && $bindings[$prefix] !== $code) { throw new \RuntimeException("Plugin prefix conflict [{$prefix}] between [{$bindings[$prefix]}] and [{$code}]"); } $bindings[$prefix] = $code; } } ksort($bindings); return sysvar(self::CACHE_BINDINGS, $bindings); } /** * 获取插件别名映射。 * @param bool $force 强制刷新 * @return array */ public static function pluginAliases(bool $force = false): array { if (!$force && is_array($aliases = sysvar(self::CACHE_ALIASES))) { return $aliases; } $aliases = []; foreach (self::allPlugins(false, $force) as $code => $plugin) { if (!empty($plugin['alias'])) { $aliases[$plugin['alias']] = $code; } } ksort($aliases); return sysvar(self::CACHE_ALIASES, $aliases); } /** * 获取插件主访问前缀。 * @param string $code 插件编码 * @param bool $force 强制刷新 */ public static function pluginPrefix(string $code, bool $force = false): string { return self::activePluginPrefix($code, $force) ?: (self::pluginPrefixes($code, $force)[0] ?? ''); } /** * 获取当前激活前缀。 * @param ?string $code 插件编码 * @param bool $force 强制刷新 */ public static function activePluginPrefix(?string $code = null, bool $force = false): string { $context = RequestContext::instance(); $current = $context->pluginCode(); $prefix = $context->pluginPrefix(); if ($current === '' || $prefix === '') { return ''; } if ($code !== null && $current !== $code) { return ''; } return in_array($prefix, self::pluginPrefixes($current, $force), true) ? $prefix : ''; } /** * 获取插件前缀集合。 * @param ?string $code 插件编码 * @param bool $force 强制刷新 * @return array>|array */ public static function pluginPrefixes(?string $code = null, bool $force = false): array { $plugins = self::allPlugins(false, $force); if ($code === null) { $items = []; foreach ($plugins as $name => $plugin) { $items[$name] = $plugin['prefixes'] ?? []; } return $items; } return $plugins[$code]['prefixes'] ?? []; } /** * 按首段路径命中插件。 * @param string $pathinfo 请求路径 * @param ?string $switch 动态插件切换 */ public static function matchPluginPath(string $pathinfo, ?string $switch = null): ?array { $pathinfo = trim($pathinfo, '\/'); $apiPrefix = self::pluginApiPrefix(); if ($pathinfo !== '' && $apiPrefix !== '') { $paths = explode('/', $pathinfo, 3); if (strval($paths[0] ?? '') === $apiPrefix) { $prefix = strval($paths[1] ?? ''); if ($prefix !== '' && ($plugin = self::resolvePluginPrefix($prefix))) { $plugin['entry'] = RequestContext::ENTRY_API; $plugin['matched_prefix'] = $prefix; $plugin['pathinfo'] = self::normalizeApiPathinfo(strval($paths[2] ?? 'index/index')); return $plugin; } } } if ($pathinfo !== '') { $paths = explode('/', $pathinfo, 2); $prefix = strval($paths[0] ?? ''); if (strpos($prefix, '.')) { $prefix = strstr($prefix, '.', true) ?: $prefix; } if ($prefix !== '' && ($plugin = self::resolvePluginPrefix($prefix))) { $plugin['entry'] = RequestContext::ENTRY_WEB; $plugin['matched_prefix'] = $prefix; $plugin['pathinfo'] = $paths[1] ?? ''; return $plugin; } } if ($switch && ($plugin = self::resolvePlugin($switch))) { $plugin['entry'] = RequestContext::ENTRY_WEB; $plugin['matched_prefix'] = ''; $plugin['pathinfo'] = $pathinfo; return $plugin; } return null; } /** * 获取 API 入口前缀。 */ public static function pluginApiPrefix(): string { if (is_string($entry = sysvar(self::CACHE_ENTRY)) && $entry !== '') { return $entry; } $entry = trim(strval(Library::$sapp->config->get('app.plugin.api_prefix', 'api')), '\/'); $entry = $entry !== '' ? $entry : 'api'; sysvar(self::CACHE_ENTRY, $entry); return $entry; } /** * 解析指定前缀绑定。 * @param ?string $prefix 路由前缀 * @param bool $append 关联安装信息 * @param bool $force 强制刷新 */ public static function resolvePluginPrefix(?string $prefix, bool $append = false, bool $force = false): ?array { if ($prefix === null || $prefix === '') { return null; } $code = self::pluginBindings($force)[$prefix] ?? null; return $code ? self::getPlugin($code, $append, $force) : null; } /** * 检测动态插件切换参数。 */ public static function detectPluginSwitch(?Request $request = null): ?string { $config = self::pluginSwitchConfig(); if (empty($config['enabled'])) { return null; } $request = $request ?: Library::$sapp->request; $header = trim(strval($config['header'] ?? '')); $query = trim(strval($config['query'] ?? '')); $value = $header === '' ? '' : trim(strval($request->header($header) ?: '')); if ($value === '' && $query !== '') { $value = trim(strval($request->get($query, ''))); } return $value === '' ? null : $value; } /** * 激活当前请求插件。 * @param null|array|string $plugin 插件编码或定义 * @param string $prefix 当前请求前缀 */ public static function activatePlugin($plugin = null, string $prefix = ''): ?array { $context = RequestContext::instance(); if (is_array($plugin)) { $code = strval($plugin['code'] ?? ''); $current = $code === '' ? null : self::resolvePlugin($code); } else { $current = empty($plugin) ? null : self::resolvePlugin(strval($plugin)); } if (empty($current)) { $context->clearPlugin()->setEntryType(RequestContext::ENTRY_WEB); return null; } $prefix = trim($prefix, '\/'); if ($prefix === '' || !in_array($prefix, $current['prefixes'] ?? [], true)) { $prefix = $current['prefixes'][0] ?? ''; } $context->setPlugin($current['code'], $prefix); return $current; } /** * 获取当前请求插件。 * @param bool $append 关联安装信息 * @param bool $force 强制刷新 */ public static function currentPlugin(bool $append = false, bool $force = false): ?array { $code = RequestContext::instance()->pluginCode(); return $code === '' ? null : self::getPlugin($code, $append, $force); } /** * 获取当前请求插件前缀。 */ public static function currentPluginPrefix(): string { return RequestContext::instance()->pluginPrefix(); } /** * 设置当前请求插件入口类型。 */ public static function activatePluginEntry(string $entryType = RequestContext::ENTRY_WEB): void { RequestContext::instance()->setEntryType($entryType); } /** * 获取当前请求插件入口类型。 */ public static function currentPluginEntry(): string { return RequestContext::instance()->entryType(); } /** * 获取当前请求插件编码。 */ public static function currentPluginCode(): string { return RequestContext::instance()->pluginCode(); } /** * 获取插件菜单定义。 * @param null|array|string $plugin 插件编码或定义 * @param bool $check 检查权限 * @param bool $normalize 标准化输出 */ public static function menus($plugin, bool $check = false, bool $normalize = false): array { if (is_array($plugin)) { $code = strval($plugin['code'] ?? ''); $current = $code === '' ? $plugin : array_replace(self::resolvePlugin($code, true) ?: [], $plugin); } else { $current = self::resolvePlugin($plugin, true); } if (empty($current['service']) || !class_exists($current['service'])) { return []; } $menus = (array)$current['service']::getMenus(); return ($check || $normalize) ? self::normalizeMenus($menus, $check) : $menus; } /** * 获取版本信息。 */ public static function getVersion(): string { $library = self::getPluginLibrarys('zoujingli/think-library'); return trim($library['version'] ?? 'v8.0.0', 'v'); } /** * 获取插件包版本信息。 * @param ?string $package 包名 * @param bool $force 强制刷新 * @return array|mixed */ public static function getPluginLibrarys(?string $package = null, bool $force = false) { $plugs = sysvar($keys = 'think.admin.version'); if (empty($plugs) || $force) { foreach (array_unique([runpath('vendor/versions.php'), syspath('vendor/versions.php')]) as $file) { if (is_file($file)) { $plugs = sysvar($keys, include $file); break; } } } return empty($package) ? $plugs : ($plugs[$package] ?? null); } /** * 生成全部静态路径。 * @param string $path 后缀路径 * @return string[] */ public static function uris(string $path = ''): array { return self::uri($path, null); } /** * 生成静态路径链接。 * @param string $path 后缀路径 * @param ?string $type 路径类型 * @param mixed $default 默认数据 * @return array|string */ public static function uri(string $path = '', ?string $type = '__ROOT__', $default = '') { $plugin = Library::$sapp->http->getName(); if (strlen($path)) { $path = '/' . ltrim($path, '/'); } $prefix = rtrim(dirname(Library::$sapp->request->basefile()), '\/'); $data = [ '__APP__' => rtrim(url('@')->build(), '\/') . $path, '__ROOT__' => $prefix . $path, '__PLUG__' => "{$prefix}/static/extra/{$plugin}{$path}", '__FULL__' => Library::$sapp->request->domain() . $prefix . $path, ]; return is_null($type) ? $data : ($data[$type] ?? $default); } /** * 打印调试数据到文件。 * @param mixed $data 输出的数据 * @param bool $new 强制替换文件 * @param null|string $file 文件名称 * @return false|int */ public static function putDebug($data, bool $new = false, ?string $file = null) { ob_start(); var_dump($data); $output = preg_replace('/]=>\n(\s+)/m', '] => ', ob_get_clean()); if (is_null($file)) { $file = runpath('runtime/' . date('Ymd') . '.log'); } elseif (!preg_match('#[/\\\]+#', $file)) { $file = runpath("runtime/{$file}.log"); } is_dir($dir = dirname($file)) or mkdir($dir, 0777, true); return $new ? file_put_contents($file, $output) : file_put_contents($file, $output, FILE_APPEND); } /** * 批量更新保存数据。 * @param Model|Query|string $query 数据查询对象 * @param array $data 需要保存的数据 * @param string $key 更新条件查询主键 * @param mixed $map 额外更新查询条件 * @return bool|int * @throws \think\admin\Exception */ public static function update($query, array $data, string $key = 'id', $map = []) { try { $query = QueryFactory::build($query)->master()->where($map); if (empty($map[$key])) { $query->where([$key => $data[$key] ?? null]); } return (clone $query)->count() > 1 ? $query->strict(false)->update($data) : $query->findOrEmpty()->save($data); } catch (\Exception|\Throwable $exception) { throw new \think\admin\Exception($exception->getMessage(), $exception->getCode()); } } /** * 数据增量保存。 * @param Model|Query|string $query 数据查询对象 * @param array $data 需要保存的数据 * @param string $key 更新条件查询主键 * @param mixed $map 额外更新查询条件 * @return bool|int * @throws \think\admin\Exception */ public static function save($query, array &$data, string $key = 'id', $map = []) { try { $query = QueryFactory::build($query)->master()->strict(false); if (empty($map[$key])) { $query->where([$key => $data[$key] ?? null]); } $model = $query->where($map)->findOrEmpty(); $action = $model->isExists() ? 'onAdminUpdate' : 'onAdminInsert'; if ($model->save($data) === false) { return false; } if ($model instanceof \think\admin\Model) { $model->{$action}(strval($model->getAttr($key))); } $data = $model->toArray(); return $model[$key] ?? true; } catch (\Exception $exception) { throw new \think\admin\Exception($exception->getMessage(), $exception->getCode()); } } /** * 扫描 app/* 本地应用目录. * * @return array> */ private static function discoverLocalApps(): array { $apps = []; $basePath = rtrim(Library::$sapp->getBasePath(), '\/') . DIRECTORY_SEPARATOR; if (!is_dir($basePath)) { return $apps; } foreach (scandir($basePath) ?: [] as $code) { if ($code === '.' || $code === '..' || in_array($code, self::IGNORE_LOCAL_APPS, true)) { continue; } if (!preg_match('/^[A-Za-z][A-Za-z0-9_]*$/', $code)) { continue; } $path = $basePath . $code . DIRECTORY_SEPARATOR; if (!is_dir($path) || !self::isLocalAppPath($path)) { continue; } $apps[$code] = self::normalize($code, [ 'type' => 'local', 'name' => ucfirst($code), 'path' => $path, 'space' => NodeService::space($code), ]); } return $apps; } /** * 判断是否为本地应用目录. */ private static function isLocalAppPath(string $path): bool { foreach (['controller', 'route', 'view', 'config'] as $name) { if (is_dir($path . $name)) { return true; } } return is_file($path . 'Service.php'); } /** * 递归收集菜单节点。 * @param array> $menus * @param array $nodes */ private static function collectMenuNodes(array $menus, array &$nodes): void { foreach ($menus as $menu) { if (!empty($menu['node'])) { $nodes[] = strval($menu['node']); } if (!empty($menu['subs'])) { self::collectMenuNodes((array)$menu['subs'], $nodes); } } } /** * 标准化插件菜单并可选按权限过滤。 * @param array> $menus * @return array> */ private static function normalizeMenus(array $menus, bool $check = false): array { foreach ($menus as $k1 => &$one) { $one['title'] = lang($one['title'] ?? ($one['name'] ?? '')); $one['url'] = $one['url'] ?? self::buildMenuUrl(strval($one['node'] ?? '')); if (!empty($one['subs'])) { foreach ($one['subs'] as $k2 => &$two) { if ($check && isset($two['node']) && !auth($two['node'])) { unset($one['subs'][$k2]); continue; } $two['title'] = lang($two['title'] ?? ($two['name'] ?? '')); $two['url'] = $two['url'] ?? self::buildMenuUrl(strval($two['node'] ?? '')); } $one['subs'] = array_values($one['subs']); } if ($check && isset($one['node']) && !auth($one['node'])) { unset($menus[$k1]); continue; } if ($one['url'] === '#' && empty($one['subs'])) { unset($menus[$k1]); } } return array_values($menus); } /** * 生成菜单 URL,缺少插件上下文时回退为系统 URL。 */ private static function buildMenuUrl(string $node): string { if ($node === '') { return '#'; } if (function_exists('plguri') && self::currentPluginCode() !== '') { return plguri($node); } return sysuri($node); } /** * 标准化应用定义. * * @param string $code 应用编号 * @param array $app 应用配置 * @return array */ private static function normalize(string $code, array $app): array { $path = $app['path'] ?? ''; $path = $path === '' ? '' : rtrim((string)$path, '\/') . DIRECTORY_SEPARATOR; return [ 'code' => $code, 'type' => $app['type'] ?? 'local', 'name' => $app['name'] ?? ucfirst($code), 'path' => $path, 'alias' => $app['alias'] ?? '', 'space' => $app['space'] ?? NodeService::space($code), 'package' => $app['package'] ?? '', 'service' => $app['service'] ?? '', ]; } /** * 标准化插件定义。 * @param string $code 插件编码 * @param array $plugin 原始定义 */ private static function normalizePlugin(string $code, array $plugin): array { $path = $plugin['path'] ?? ''; $path = $path === '' ? '' : rtrim((string)$path, '\/') . DIRECTORY_SEPARATOR; $prefixes = self::effectivePluginPrefixes($code, $plugin); return [ 'code' => $code, 'type' => $plugin['type'] ?? 'plugin', 'name' => $plugin['name'] ?? ucfirst($code), 'path' => $path, 'alias' => $plugin['alias'] ?? '', 'prefix' => $prefixes[0] ?? '', 'prefixes' => $prefixes, 'space' => $plugin['space'] ?? '', 'package' => $plugin['package'] ?? '', 'service' => $plugin['service'] ?? '', 'document' => $plugin['document'] ?? '', 'description' => $plugin['description'] ?? '', 'platforms' => (array)($plugin['platforms'] ?? []), 'license' => (array)($plugin['license'] ?? []), 'version' => strval($plugin['version'] ?? ''), 'homepage' => strval($plugin['homepage'] ?? ''), 'show' => !array_key_exists('show', $plugin) || !empty($plugin['show']), ]; } /** * 获取插件有效前缀。 * @param string $code 插件编码 * @param array $plugin 插件定义 * @return string[] */ private static function effectivePluginPrefixes(string $code, array $plugin): array { $prefixes = self::configuredPluginPrefixes($code); if ($prefixes === null) { $prefixes = self::normalizePluginPrefixes($plugin['prefixes'] ?? [], $plugin['prefix'] ?? '', $plugin['alias'] ?? '', $code); } if (empty($prefixes)) { $prefixes = [$code]; } return $prefixes; } /** * 获取配置文件中的插件前缀定义。 * @param string $code 插件编码 * @return null|string[] */ private static function configuredPluginPrefixes(string $code): ?array { $config = (array)Library::$sapp->config->get('app.plugin.bindings', []); if (array_key_exists($code, $config)) { return self::normalizePluginPrefixes($config[$code]); } foreach ($config as $item) { if (!is_array($item)) { continue; } $name = strval($item['code'] ?? $item['plugin'] ?? ''); if ($name === $code) { return self::normalizePluginPrefixes($item['prefixes'] ?? ($item['prefix'] ?? [])); } } return null; } /** * 标准化前缀集合。 * @param mixed ...$values 原始前缀 * @return string[] */ private static function normalizePluginPrefixes(...$values): array { $items = []; foreach ($values as $value) { foreach ((array)$value as $prefix) { $prefix = trim((string)$prefix, " \t\n\r\0\x0B\\/"); if ($prefix === '') { continue; } if (strpos($prefix, '/')) { $prefix = strstr($prefix, '/', true) ?: $prefix; } if (strpos($prefix, '.')) { $prefix = strstr($prefix, '.', true) ?: $prefix; } if ($prefix !== '' && !in_array($prefix, $items, true)) { $items[] = $prefix; } } } return $items; } /** * 附加插件安装信息。 * @param array $plugin 插件定义 */ private static function appendPluginInstall(array $plugin): array { $versions = self::getPluginLibrarys(); $plugin['install'] = $versions[$plugin['package']] ?? []; foreach (['type', 'name', 'document', 'description', 'homepage', 'version'] as $field) { if (empty($plugin[$field])) { $plugin[$field] = $plugin['install'][$field] ?? ($field === 'type' ? 'plugin' : ''); } } $plugin['platforms'] = array_values(array_unique(array_filter(array_merge( (array)($plugin['platforms'] ?? []), (array)($plugin['install']['platforms'] ?? []) )))); $plugin['license'] = array_values(array_unique(array_filter(array_merge( (array)($plugin['license'] ?? []), (array)($plugin['install']['license'] ?? []) )))); return $plugin; } /** * 获取插件切换配置。 * @return array{enabled:bool,query:string,header:string} */ private static function pluginSwitchConfig(): array { $config = (array)Library::$sapp->config->get('app.plugin.switch', []); return [ 'enabled' => isset($config['enabled']) ? boolval($config['enabled']) : false, 'query' => strval($config['query'] ?? '_plugin'), 'header' => strval($config['header'] ?? 'X-Plugin-App'), ]; } /** * 标准化 API 入口路径。 * /api/{plugin}/upload/file -> api.upload/file. */ private static function normalizeApiPathinfo(string $pathinfo): string { $pathinfo = trim($pathinfo, '\/'); if ($pathinfo === '') { return 'api.index/index'; } if (strpos($pathinfo, 'api.') === 0) { return $pathinfo; } [$controller, $action] = array_pad(explode('/', $pathinfo, 2), 2, 'index'); $controller = trim(strtr($controller, '/', '.'), '.'); return 'api.' . $controller . '/' . trim($action, '\/'); } }