init($rules, $type, $callable); } } if (!function_exists('_query')) { /** * 创建快捷查询构造器。 * * @param BaseQuery|Model|string $dbQuery 查询对象或模型名称 * @param null|array|string $input 附加输入条件 */ function _query(BaseQuery|Model|string $dbQuery, array|string|null $input = null): QueryHelper { return QueryHelper::instance()->init($dbQuery, $input); } } if (!function_exists('sysvar')) { /** * 读写单次请求内的轻量级内存变量。 * * 仅用于当前请求周期内的临时缓存。 * 传入空字符串 `('', '')` 时会清空全部缓存。 * * @param ?string $name 变量名 * @param null|mixed $value 变量值 * @return mixed */ function sysvar(?string $name = null, mixed $value = null) { static $swap = []; if ($name === '' && $value === '') { return $swap = []; } if ($value === null) { return $name === null ? $swap : ($swap[$name] ?? null); } return $swap[$name] = $value; } } if (!function_exists('sysuri')) { /** * 生成系统页面 URL。 * * 参数与 ThinkPHP `url()` 保持一致, * 但会在构建前先把后台页面地址标准化为短链目标。 * * @param string $url 路由地址 * @param array $vars 路由参数 * @param bool|string $suffix 后缀配置 * @param bool|string $domain 域名配置 */ function sysuri(string $url = '', array $vars = [], bool|string $suffix = true, bool|string $domain = false): string { $target = Url::normalizeWebTarget($url); return Library::$sapp->route->buildUrl($target, $vars)->suffix($suffix)->domain($domain)->build(); } } if (!function_exists('apiuri')) { /** * 生成标准插件 API URL。 * * 统一输出 `/api/{plugin}/{controller}/{action}` 风格地址, * 并兼容当前插件上下文与 `controller/api/*` 的历史写法。 * * @param string $url 路由地址 * @param array $vars 路由参数 * @param bool|string $suffix 后缀配置 * @param bool|string $domain 域名配置 */ function apiuri(string $url = '', array $vars = [], bool|string $suffix = true, bool|string $domain = false): string { $target = Url::normalizeApiTarget($url); return Library::$sapp->route->buildUrl($target, $vars)->suffix($suffix)->domain($domain)->build(); } } if (!function_exists('tsession')) { /** * 获取令牌会话服务实例。 */ function tsession(): CacheSession { return CacheSession::instance(); } } if (!function_exists('encode')) { /** * 将 UTF-8 文本编码为兼容旧逻辑的短字符串。 */ function encode(string $content): string { $string = CodeToolkit::text2utf8($content); $length = strlen($string); if ($length === 0) { return ''; } $chars = ''; for ($i = 0; $i < $length; ++$i) { $chars .= str_pad(base_convert((string)ord($string[$i]), 10, 36), 2, '0', STR_PAD_LEFT); } return $chars; } } if (!function_exists('decode')) { /** * 将 `encode()` 结果还原为 UTF-8 文本。 */ function decode(string $content): string { if ($content === '') { return ''; } $chars = ''; foreach (str_split($content, 2) as $char) { if (strlen($char) < 2) { continue; } $chars .= chr((int)base_convert($char, 36, 10)); } return CodeToolkit::text2utf8($chars); } } if (!function_exists('str2arr')) { /** * 将字符串或数组标准化为数组。 * * 字符串会按分隔符拆分;数组会递归展开。 * 返回结果会自动去空白,并按需执行 allow 白名单过滤。 * * @param array|string $text 原始内容 * @param string $separ 分隔符 * @param ?array $allow 白名单限制 */ function str2arr(array|string $text, string $separ = ',', ?array $allow = null): array { $items = []; foreach ((array)$text as $item) { if (is_array($item)) { foreach (str2arr($item, $separ, $allow) as $value) { $items[] = $value; } continue; } if (!is_scalar($item) || $item === false) { continue; } if (is_string($item)) { foreach (explode($separ, trim($item, $separ)) as $value) { $value = trim($value); if ($value !== '' && (!is_array($allow) || in_array($value, $allow, true))) { $items[] = $value; } } continue; } if (!is_array($allow) || in_array($item, $allow, true)) { $items[] = $item; } } return $items; } } if (!function_exists('arr2str')) { /** * 将字符串或数组标准化为分隔字符串。 * * 内部会复用 `str2arr()` 做统一归一化, * 最终输出形如 `,a,b,c,` 的历史兼容格式。 * * @param array|string $data 原始内容 * @param string $separ 分隔符 * @param ?array $allow 白名单限制 */ function arr2str(array|string $data, string $separ = ',', ?array $allow = null): string { $items = str2arr($data, $separ, $allow); return empty($items) ? '' : $separ . join($separ, $items) . $separ; } } if (!function_exists('format_datetime')) { /** * 兼容旧版时间格式化函数。 * * @param mixed $value 原始时间值 * @param string $format 输出格式 */ function format_datetime(mixed $value, string $format = 'Y-m-d H:i:s'): string { if ($value === null || $value === '' || $value === false) { return '-'; } if (is_numeric($value)) { $timestamp = intval($value); } else { $timestamp = strtotime(strval($value)) ?: 0; } return $timestamp > 0 ? date($format, $timestamp) : '-'; } } if (!function_exists('isDebug')) { /** * 判断当前是否处于调试模式。 */ function isDebug(): bool { return RuntimeService::isDebug(); } } if (!function_exists('isOnline')) { /** * 判断当前是否处于生产模式。 */ function isOnline(): bool { return RuntimeService::isOnline(); } } if (!function_exists('syspath')) { /** * 获取系统路径(兼容 Phar 的内部路径). * * 直接读取打包在 Phar 包内的文件。 * - PHAR 环境:返回 phar:// 协议路径(只读) * - 普通环境:返回实际文件系统路径 * * 示例:syspath('config') 读取 phar 包内的 config 目录 * * @param string $path 相对路径 * @param ?string $root 自定义根路径(一般不传) * @return string 完整的系统路径(Phar 内部路径或文件系统路径) */ function syspath(string $path = '', ?string $root = null): string { // 如果未提供 root,自动检测运行环境 if ($root === null) { // Phar::running(false) 返回物理路径(不含 phar://),避免出现 phar://phar:// 这种重复前缀 $phar = Phar::running(false); $root = $phar !== '' ? "phar://{$phar}" : Library::$sapp->getRootPath(); } $root = rtrim(strval($root), '/\\'); return $path === '' ? $root : "{$root}/" . ltrim(str_replace('\\', '/', $path), '/'); } } if (!function_exists('runpath')) { /** * 获取运行时路径(操作系统文件系统路径). * * 直接读取/写入操作系统的文件。 * - PHAR 环境:返回 Phar 包外部的安装目录(可写) * - 普通环境:返回项目根目录(可写) * * 示例:runpath('runtime') 读取/写入操作系统文件系统的 runtime 目录 * * @param string $path 相对路径 * @return string 完整的运行时路径(操作系统文件系统路径) */ function runpath(string $path = ''): string { $phar = Phar::running(false); $base = rtrim($phar !== '' ? dirname($phar) : Library::$sapp->getRootPath(), '/\\'); return $path === '' ? $base : (($path === '/') ? $base : "{$base}/" . ltrim(str_replace('\\', '/', $path), '/')); } } if (!function_exists('is_phar')) { /** * 判断当前是否运行在 PHAR 环境中。 */ function is_phar(): bool { static $cache = null; if ($cache === null) { $cache = Phar::running() !== ''; } return $cache; } } if (!function_exists('enbase64url')) { /** * Base64 URL 安全编码。 */ function enbase64url(string $string): string { return CodeToolkit::enSafe64($string); } } if (!function_exists('debase64url')) { /** * Base64 URL 安全解码。 */ function debase64url(string $string): string { return CodeToolkit::deSafe64($string); } } if (!function_exists('xss_safe')) { /** * 对文本执行基础 XSS 安全处理。 * * 当前逻辑会移除 script 标签,并中和内联事件属性。 */ function xss_safe(string $text): string { $rules = [ '#]*>.*?#is' => '', '#(\s+)on([a-z_][\w:-]*\s*=)#i' => '$1data-on-$2', ]; return preg_replace(array_keys($rules), array_values($rules), trim($text)) ?? trim($text); } } if (!function_exists('password_mask')) { /** * 返回编辑态密码占位符。 * * 默认固定为 6 个星号,前端展示统一使用该值, * 用户保留全星号提交时可视为“不修改密码”。 */ function password_mask(int $length = 6): string { return str_repeat('*', max(1, $length)); } } if (!function_exists('password_is_mask')) { /** * 判断输入是否为纯星号密码占位。 * * 只要输入内容全部由 `*` 组成,且长度大于 0, * 就认为它是保留旧密码的占位内容。 */ function password_is_mask(mixed $value): bool { $value = trim(strval($value)); return $value !== '' && preg_match('/^\*+$/', $value) === 1; } } if (!function_exists('password_is_unchanged')) { /** * 判断编辑态密码输入是否表示“保持不变”。 * * 兼容两种历史/新标准: * - 空字符串:历史“留空不修改” * - 纯星号:新标准“保留星号不修改” */ function password_is_unchanged(mixed $value): bool { $value = trim(strval($value)); return $value === '' || password_is_mask($value); } } if (!function_exists('http_get')) { /** * 发送 GET 请求。 * * @param string $url 请求地址 * @param array|string $query 查询参数 * @param array $options 客户端配置 */ function http_get(string $url, array|string $query = [], array $options = []): bool|string { return HttpClient::get($url, $query, $options); } } if (!function_exists('http_post')) { /** * 发送 POST 请求。 * * @param string $url 请求地址 * @param array|string $data 提交数据 * @param array $options 客户端配置 */ function http_post(string $url, array|string $data, array $options = []): bool|string { return HttpClient::post($url, $data, $options); } } if (!function_exists('data_save')) { /** * 按主键或条件执行增量保存。 * * @param Model|Query|string $dbQuery 查询对象或模型 * @param array $data 保存数据 * @param string $key 主键字段 * @param null|array $where 附加条件 * @throws Exception */ function data_save(Model|Query|string $dbQuery, array $data, string $key = 'id', ?array $where = []): bool|int { return AppService::save($dbQuery, $data, $key, $where); } } if (!function_exists('down_file')) { /** * 下载远程文件并返回本地访问地址。 * * @param string $source 源文件地址 * @param bool $force 是否强制重下 * @param int $expire 本地缓存秒数 */ function down_file(string $source, bool $force = false, int $expire = 0): string { return Storage::down($source, $force, $expire)['url'] ?? $source; } } if (!function_exists('trace_file')) { /** * 将异常信息落盘到 runtime/trace 目录。 * * @param Throwable $exception 异常对象 */ function trace_file(Throwable $exception): bool { $path = rtrim(Library::$sapp->getRuntimePath(), '\/') . DIRECTORY_SEPARATOR . 'trace'; if (!is_dir($path) && !mkdir($path, 0777, true) && !is_dir($path)) { return false; } $root = strtr(rtrim(syspath(), '\/'), '\\', '/'); $source = strtr($exception->getFile(), '\\', '/'); $name = basename($source); if ($root !== '' && str_starts_with(strtolower($source), strtolower($root . '/'))) { $name = ltrim(substr($source, strlen($root)), '/'); } $file = $path . DIRECTORY_SEPARATOR . date('Ymd_His_') . strtr($name, ['/' => '.', '\\' => '.']); $json = json_encode( $exception instanceof Exception ? $exception->getData() : [], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE ) ?: '[]'; $class = get_class($exception); return file_put_contents( $file, "[CODE] {$exception->getCode()}" . PHP_EOL . "[INFO] {$exception->getMessage()}" . PHP_EOL . ($exception instanceof Exception ? "[DATA] {$json}" . PHP_EOL : '') . "[FILE] {$class} in {$name} line {$exception->getLine()}" . PHP_EOL . '[TIME] ' . date('Y-m-d H:i:s') . PHP_EOL . PHP_EOL . '[TRACE]' . PHP_EOL . $exception->getTraceAsString() ) !== false; } } if (!function_exists('format_bytes')) { /** * 将字节数格式化为可读单位。 * * @param float|int|string $size 原始字节值 */ function format_bytes(float|int|string $size): string { if (is_numeric($size)) { $size = (float)$size; $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; for ($i = 0; $size >= 1024 && $i < count($units) - 1; ++$i) { $size /= 1024; } return round($size, 2) . ' ' . $units[$i]; } return is_string($size) ? $size : strval($size); } }