Anyon e7a8c05556 chore(repo): 统一 v8 仓库品牌名称
将 v8 重构分支中残留的 ThinkAdminDeveloper 文本统一调整为 ThinkAdmin,避免迁移到主仓库后继续暴露旧开发仓库名称。

主要内容:

- 更新 README 标题与项目描述。

- 统一 PHP 文件头注释中的项目标识。

- 同步调整测试、配置、插件与文档中的旧仓库名称文本。

- 保持旧包删除说明与架构边界测试语义不变,只清理品牌名称残留。
2026-05-08 16:15:24 +08:00

570 lines
17 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
/**
* +----------------------------------------------------------------------
* | ThinkAdmin Plugin
* +----------------------------------------------------------------------
* | Copyright (c) 2014~2026 ThinkAdmin [ thinkadmin.top ]
* +----------------------------------------------------------------------
* | Official Website: https://thinkadmin.top
* +----------------------------------------------------------------------
* | Licensed: https://mit-license.org
* | Disclaimer: https://thinkadmin.top/disclaimer
* | Vip Rights: https://thinkadmin.top/vip-introduce
* +----------------------------------------------------------------------
* | Gitee Repository: https://gitee.com/zoujingli/ThinkAdmin
* | Github Repository: https://github.com/zoujingli/ThinkAdmin
* +----------------------------------------------------------------------
*/
use think\admin\Exception;
use think\admin\extend\CodeToolkit;
use think\admin\extend\HttpClient;
use think\admin\helper\QueryHelper;
use think\admin\helper\ValidateHelper;
use think\admin\Library;
use think\admin\model\ModelFactory;
use think\admin\route\Url;
use think\admin\service\AppService;
use think\admin\service\CacheSession;
use think\admin\service\RuntimeService;
use think\admin\Storage;
use think\db\BaseQuery;
use think\db\Query;
use think\Model;
if (!function_exists('p')) {
/**
* 输出调试数据到运行日志文件。
*
* @param mixed $data 调试数据
* @param bool $new 是否覆盖原文件
* @param ?string $file 指定日志文件
*/
function p(mixed $data, bool $new = false, ?string $file = null): false|int
{
return AppService::putDebug($data, $new, $file);
}
}
if (!function_exists('m')) {
/**
* 动态创建模型实例。
*
* @param string $name 模型名称
* @param array $data 初始数据
* @param string $conn 指定连接
*/
function m(string $name, array $data = [], string $conn = ''): Model
{
return ModelFactory::build($name, $data, $conn);
}
}
if (!function_exists('_vali')) {
/**
* 快捷读取输入并执行验证。
*
* @param array $rules 验证规则
* @param array|string $type 输入源或输入数据
* @param ?callable $callable 验证失败回调
*/
function _vali(array $rules, array|string $type = '', ?callable $callable = null): array
{
return ValidateHelper::instance()->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 = [
'#<script\b[^>]*>.*?</script>#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);
}
}