chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
// server/utils/AesUtil.php
namespace ywxapp\utils;
/**
* AesUtil 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class AesUtil
{
private const METHOD = 'aes-256-cbc';
private static $key;
private static $iv;
public static function setKeyAndIv(string $key, string $iv)
{
if (strlen($key) !== 32) throw new \Exception("AES Key 必须为32字节");
if (strlen($iv) !== 16) throw new \Exception("AES IV 必须为16字节");
self::$key = $key;
self::$iv = $iv;
}
public static function encrypt(string $plaintext): string
{
$encrypted = openssl_encrypt(
$plaintext,
self::METHOD,
self::$key,
OPENSSL_RAW_DATA,
self::$iv
);
return base64_encode($encrypted);
}
public static function decrypt(string $ciphertext): string
{
$data = base64_decode($ciphertext);
$result = openssl_decrypt(
$data,
self::METHOD,
self::$key,
OPENSSL_RAW_DATA,
self::$iv
);
if ($result === false) throw new \Exception("AES 解密失败");
return $result;
}
}
+218
View File
@@ -0,0 +1,218 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace ywxapp\utils;
use DateTime;
use DateTimeZone;
/**
* 日期时间处理类.
*/
class Date
{
const YEAR = 31536000;
const MONTH = 2592000;
const WEEK = 604800;
const DAY = 86400;
const HOUR = 3600;
const MINUTE = 60;
/**
* 计算两个时区间相差的时长,单位为秒.
*
* $seconds = self::offset('America/Chicago', 'GMT');
*
* [!!] A list of time zones that PHP supports can be found at
* <http://php.net/timezones>.
*
* @param string $remote timezone that to find the offset of
* @param string $local timezone used as the baseline
* @param mixed $now UNIX timestamp or date string
*
* @return int
*/
public static function offset($remote, $local = null, $now = null)
{
if ($local === null) {
// Use the default timezone
$local = date_default_timezone_get();
}
if (is_int($now)) {
// Convert the timestamp into a string
$now = date(DateTime::RFC2822, $now);
}
// Create timezone objects
$zone_remote = new DateTimeZone($remote);
$zone_local = new DateTimeZone($local);
// Create date objects from timezones
$time_remote = new DateTime($now, $zone_remote);
$time_local = new DateTime($now, $zone_local);
// Find the offset
$offset = $zone_remote->getOffset($time_remote) - $zone_local->getOffset($time_local);
return $offset;
}
/**
* 计算两个时间戳之间相差的时间.
*
* $span = self::span(60, 182, 'minutes,seconds'); // array('minutes' => 2, 'seconds' => 2)
* $span = self::span(60, 182, 'minutes'); // 2
*
* @param int $remote timestamp to find the span of
* @param int $local timestamp to use as the baseline
* @param string $output formatting string
*
* @return string when only a single output is requested
* @return array associative list of all outputs requested
* @from https://github.com/kohana/ohanzee-helpers/blob/master/src/Date.php
*/
public static function span($remote, $local = null, $output = 'years,months,weeks,days,hours,minutes,seconds')
{
// Normalize output
$output = trim(strtolower((string) $output));
if (! $output) {
// Invalid output
return false;
}
// Array with the output formats
$output = preg_split('/[^a-z]+/', $output);
// Convert the list of outputs to an associative array
$output = array_combine($output, array_fill(0, count($output), 0));
// Make the output values into keys
extract(array_flip($output), EXTR_SKIP);
if ($local === null) {
// Calculate the span from the current time
$local = time();
}
// Calculate timespan (seconds)
$timespan = abs($remote - $local);
if (isset($output['years'])) {
$timespan -= self::YEAR * ($output['years'] = (int) floor($timespan / self::YEAR));
}
if (isset($output['months'])) {
$timespan -= self::MONTH * ($output['months'] = (int) floor($timespan / self::MONTH));
}
if (isset($output['weeks'])) {
$timespan -= self::WEEK * ($output['weeks'] = (int) floor($timespan / self::WEEK));
}
if (isset($output['days'])) {
$timespan -= self::DAY * ($output['days'] = (int) floor($timespan / self::DAY));
}
if (isset($output['hours'])) {
$timespan -= self::HOUR * ($output['hours'] = (int) floor($timespan / self::HOUR));
}
if (isset($output['minutes'])) {
$timespan -= self::MINUTE * ($output['minutes'] = (int) floor($timespan / self::MINUTE));
}
// Seconds ago, 1
if (isset($output['seconds'])) {
$output['seconds'] = $timespan;
}
if (count($output) === 1) {
// Only a single output was requested, return it
return array_pop($output);
}
// Return array
return $output;
}
/**
* 格式化 UNIX 时间戳为人易读的字符串.
*
* @param int Unix 时间戳
* @param mixed $local 本地时间
*
* @return string 格式化的日期字符串
*/
public static function human($remote, $local = null)
{
$time_diff = (is_null($local) || $local ? time() : $local) - $remote;
$tense = $time_diff < 0 ? 'after' : 'ago';
$time_diff = abs($time_diff);
$chunks = [
[60 * 60 * 24 * 365, 'year'],
[60 * 60 * 24 * 30, 'month'],
[60 * 60 * 24 * 7, 'week'],
[60 * 60 * 24, 'day'],
[60 * 60, 'hour'],
[60, 'minute'],
[1, 'second']
];
$name = 'second';
$count = 0;
for ($i = 0, $j = count($chunks); $i < $j; $i++) {
$seconds = $chunks[$i][0];
$name = $chunks[$i][1];
if (($count = floor($time_diff / $seconds)) != 0) {
break;
}
}
return __("%d $name%s $tense", $count, ($count > 1 ? 's' : ''));
}
/**
* 获取一个基于时间偏移的Unix时间戳.
*
* @param string $type 时间类型,默认为day,可选minute,hour,day,week,month,quarter,year
* @param int $offset 时间偏移量 默认为0,正数表示当前type之后,负数表示当前type之前
* @param string $position 时间的开始或结束,默认为begin,可选前(begin,start,first,front)end
* @param int $year 基准年,默认为null,即以当前年为基准
* @param int $month 基准月,默认为null,即以当前月为基准
* @param int $day 基准天,默认为null,即以当前天为基准
* @param int $hour 基准小时,默认为null,即以当前年小时基准
* @param int $minute 基准分钟,默认为null,即以当前分钟为基准
*
* @return int 处理后的Unix时间戳
*/
public static function unixtime($type = 'day', $offset = 0, $position = 'begin', $year = null, $month = null, $day = null, $hour = null, $minute = null)
{
$year = is_null($year) ? date('Y') : $year;
$month = is_null($month) ? date('m') : $month;
$day = is_null($day) ? date('d') : $day;
$hour = is_null($hour) ? date('H') : $hour;
$minute = is_null($minute) ? date('i') : $minute;
$position = in_array($position, ['begin', 'start', 'first', 'front']);
switch ($type) {
case 'minute':
$time = $position ? mktime($hour, $minute + $offset, 0, $month, $day, $year) : mktime($hour, $minute + $offset, 59, $month, $day, $year);
break;
case 'hour':
$time = $position ? mktime($hour + $offset, 0, 0, $month, $day, $year) : mktime($hour + $offset, 59, 59, $month, $day, $year);
break;
case 'day':
$time = $position ? mktime(0, 0, 0, $month, $day + $offset, $year) : mktime(23, 59, 59, $month, $day + $offset, $year);
break;
case 'week':
$time = $position ?
mktime(0, 0, 0, $month, $day - date('w', mktime(0, 0, 0, $month, $day, $year)) + 1 - 7 * (-$offset), $year) :
mktime(23, 59, 59, $month, $day - date('w', mktime(0, 0, 0, $month, $day, $year)) + 7 - 7 * (-$offset), $year);
break;
case 'month':
$time = $position ? mktime(0, 0, 0, $month + $offset, 1, $year) : mktime(23, 59, 59, $month + $offset, cal_days_in_month(CAL_GREGORIAN, $month + $offset, $year), $year);
break;
case 'quarter':
$time = $position ?
mktime(0, 0, 0, 1 + ((ceil(date('n', mktime(0, 0, 0, $month, $day, $year)) / 3) + $offset) - 1) * 3, 1, $year) :
mktime(23, 59, 59, (ceil(date('n', mktime(0, 0, 0, $month, $day, $year)) / 3) + $offset) * 3, cal_days_in_month(CAL_GREGORIAN, (ceil(date('n', mktime(0, 0, 0, $month, $day, $year)) / 3) + $offset) * 3, $year), $year);
break;
case 'year':
$time = $position ? mktime(0, 0, 0, 1, 1, $year + $offset) : mktime(23, 59, 59, 12, 31, $year + $offset);
break;
default:
$time = mktime($hour, $minute, 0, $month, $day, $year);
break;
}
return $time;
}
}
File diff suppressed because one or more lines are too long
+185
View File
@@ -0,0 +1,185 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace ywxapp\utils;
/**
* 微信公众号消息加解密类 (JSON数组版) - 互通修复版
* ✅ 严格PKCS#7填充验证 | ✅ 二进制安全随机数 | ✅ JSON字节一致性保障
* 实测环境:PHP 7.4+ / Node.js 18+ / Chrome 115+
*/
class JsonCrypto
{
private $token;
private $encodingAesKey;
private $appId;
const BLOCK_SIZE = 32;
public function __construct($token, $encodingAesKey, $appId)
{
if (empty($token) || empty($appId) || strlen($encodingAesKey) !== 43) {
throw new Exception("配置错误:Token/AppID不能为空,EncodingAESKey需为43位Base64字符串");
}
// 严格验证Base64字符集(防空格/换行等污染)
if (!preg_match('/^[A-Za-z0-9+\/]{43}$/', $encodingAesKey)) {
throw new Exception("EncodingAESKey包含非法字符(仅允许A-Z, a-z, 0-9, +, /");
}
$this->token = $token;
$this->encodingAesKey = base64_decode($encodingAesKey . '=', true);
if ($this->encodingAesKey === false || strlen($this->encodingAesKey) !== 32) {
throw new Exception("EncodingAESKey解码失败或长度错误(应为32字节)");
}
$this->appId = $appId;
}
/**
* 加密消息 (数组 -> 标准JSON -> 拼接 -> PKCS#7 -> AES)
* ✅ 与JS完全对齐:JSON无空格、无转义、严格UTF-8字节
*/
public function encrypt(array $data): string
{
// 核心修复1:强制生成与JS完全一致的JSON字节流
$jsonPayload = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($jsonPayload === false) {
throw new Exception("JSON编码失败: " . json_last_error_msg());
}
// 移除所有空格/换行(确保与JS的JSON.stringify(data, null, 0)字节一致)
$jsonPayload = preg_replace('/\s+/', '', $jsonPayload);
// 核心修复2:使用二进制安全随机数(与JS crypto.getRandomValues 对齐)
$random = random_bytes(16); // PHP 7.0+ 原生支持
// 拼接明文: random(16) + len(4, big-endian) + json + appId
$msgLength = strlen($jsonPayload);
$msgLengthBin = pack('N', $msgLength); // 大端序4字节
$toBeEncrypted = $random . $msgLengthBin . $jsonPayload . $this->appId;
// PKCS#7填充
$paddedText = $this->pkcs7Pad($toBeEncrypted);
// AES-256-CBC加密(IV=Key前16字节,无填充)
$encrypted = openssl_encrypt(
$paddedText,
'AES-256-CBC',
$this->encodingAesKey,
OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
substr($this->encodingAesKey, 0, 16)
);
if ($encrypted === false) {
throw new Exception("AES加密失败: " . openssl_error_string());
}
return base64_encode($encrypted);
}
/**
* 解密消息 (Base64 -> AES -> PKCS#7验证 -> 解析)
* ✅ 严格填充验证 | ✅ 长度边界检查 | ✅ AppID原始字节比对
*/
public function decrypt(string $encryptedBase64): ?array
{
$ciphertext = base64_decode($encryptedBase64, true);
if ($ciphertext === false || strlen($ciphertext) < 32) {
return null;
}
$decrypted = openssl_decrypt(
$ciphertext,
'AES-256-CBC',
$this->encodingAesKey,
OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
substr($this->encodingAesKey, 0, 16)
);
if ($decrypted === false) {
return null;
}
// 核心修复3:严格PKCS#7填充验证(与JS逻辑完全一致)
$unpadded = $this->pkcs7Unpad($decrypted);
if ($unpadded === null || strlen($unpadded) < 20) {
return null;
}
// 解析长度字段(大端序4字节)
$msgLengthBin = substr($unpadded, 16, 4);
if (strlen($msgLengthBin) !== 4) return null;
$msgLength = unpack('N', $msgLengthBin)[1];
// 严格边界检查(防越界)
if ($msgLength <= 0 || (20 + $msgLength) > strlen($unpadded)) {
return null;
}
// 提取JSON和AppID(原始字节比对,禁止trim/过滤)
$jsonPayload = substr($unpadded, 20, $msgLength);
$fromAppId = substr($unpadded, 20 + $msgLength);
// 核心修复4:AppID原始字节比对(禁止字符过滤!)
if ($fromAppId !== $this->appId) {
// 调试用(生产环境建议移除)
error_log("AppID mismatch! Expected: " . bin2hex($this->appId) . ", Got: " . bin2hex($fromAppId));
return null;
}
$data = json_decode($jsonPayload, true, 512, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
return is_array($data) ? $data : null;
}
/**
* 生成签名(与JS逻辑严格一致)
*/
public function generateSignature($timestamp, $nonce, $encryptMsg): string
{
$tmpArr = [$this->token, (string)$timestamp, (string)$nonce, (string)$encryptMsg];
sort($tmpArr, SORT_STRING);
return sha1(implode('', $tmpArr));
}
// =============== 私有辅助方法(关键修复区) ===============
/**
* PKCS#7填充(安全实现)
*/
private function pkcs7Pad(string $text): string
{
$padLen = self::BLOCK_SIZE - (strlen($text) % self::BLOCK_SIZE);
return $text . str_repeat(chr($padLen), $padLen);
}
/**
* PKCS#7严格验证与移除(核心修复!)
* ✅ 验证填充长度范围
* ✅ 验证所有填充字节值一致性
* ✅ 验证填充长度不超过原文长度
* ❌ 任何异常立即返回null(与JS行为一致)
*/
private function pkcs7Unpad(string $text): ?string
{
$textLen = strlen($text);
if ($textLen === 0) return null;
$pad = ord($text[$textLen - 1]);
// 严格验证1:填充长度合法性
if ($pad < 1 || $pad > self::BLOCK_SIZE || $pad > $textLen) {
return null;
}
// 严格验证2:所有填充字节必须等于$pad值
for ($i = $textLen - $pad; $i < $textLen; $i++) {
if (ord($text[$i]) !== $pad) {
return null;
}
}
return substr($text, 0, -$pad);
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace ywxapp\utils;
/**
* 中文转拼音类.
*/
class Pinyin
{
protected static $keys = 'a|ai|an|ang|ao|ba|bai|ban|bang|bao|bei|ben|beng|bi|bian|biao|bie|bin|bing|bo|bu|ca|cai|can|cang|cao|ce|ceng|cha|chai|chan|chang|chao|che|chen|cheng|chi|chong|chou|chu|chuai|chuan|chuang|chui|chun|chuo|ci|cong|cou|cu|cuan|cui|cun|cuo|da|dai|dan|dang|dao|de|deng|di|dian|diao|die|ding|diu|dong|dou|du|duan|dui|dun|duo|e|en|er|fa|fan|fang|fei|fen|feng|fo|fou|fu|ga|gai|gan|gang|gao|ge|gei|gen|geng|gong|gou|gu|gua|guai|guan|guang|gui|gun|guo|ha|hai|han|hang|hao|he|hei|hen|heng|hong|hou|hu|hua|huai|huan|huang|hui|hun|huo|ji|jia|jian|jiang|jiao|jie|jin|jing|jiong|jiu|ju|juan|jue|jun|ka|kai|kan|kang|kao|ke|ken|keng|kong|kou|ku|kua|kuai|kuan|kuang|kui|kun|kuo|la|lai|lan|lang|lao|le|lei|leng|li|lia|lian|liang|liao|lie|lin|ling|liu|long|lou|lu|lv|luan|lue|lun|luo|ma|mai|man|mang|mao|me|mei|men|meng|mi|mian|miao|mie|min|ming|miu|mo|mou|mu|na|nai|nan|nang|nao|ne|nei|nen|neng|ni|nian|niang|niao|nie|nin|ning|niu|nong|nu|nv|nuan|nue|nuo|o|ou|pa|pai|pan|pang|pao|pei|pen|peng|pi|pian|piao|pie|pin|ping|po|pu|qi|qia|qian|qiang|qiao|qie|qin|qing|qiong|qiu|qu|quan|que|qun|ran|rang|rao|re|ren|reng|ri|rong|rou|ru|ruan|rui|run|ruo|sa|sai|san|sang|sao|se|sen|seng|sha|shai|shan|shang|shao|she|shen|sheng|shi|shou|shu|shua|shuai|shuan|shuang|shui|shun|shuo|si|song|sou|su|suan|sui|sun|suo|ta|tai|tan|tang|tao|te|teng|ti|tian|tiao|tie|ting|tong|tou|tu|tuan|tui|tun|tuo|wa|wai|wan|wang|wei|wen|weng|wo|wu|xi|xia|xian|xiang|xiao|xie|xin|xing|xiong|xiu|xu|xuan|xue|xun|ya|yan|yang|yao|ye|yi|yin|ying|yo|yong|you|yu|yuan|yue|yun|za|zai|zan|zang|zao|ze|zei|zen|zeng|zha|zhai|zhan|zhang|zhao|zhe|zhen|zheng|zhi|zhong|zhou|zhu|zhua|zhuai|zhuan|zhuang|zhui|zhun|zhuo|zi|zong|zou|zu|zuan|zui|zun|zuo';
protected static $values = '-20319|-20317|-20304|-20295|-20292|-20283|-20265|-20257|-20242|-20230|-20051|-20036|-20032|-20026|-20002|-19990|-19986|-19982|-19976|-19805|-19784|-19775|-19774|-19763|-19756|-19751|-19746|-19741|-19739|-19728|-19725|-19715|-19540|-19531|-19525|-19515|-19500|-19484|-19479|-19467|-19289|-19288|-19281|-19275|-19270|-19263|-19261|-19249|-19243|-19242|-19238|-19235|-19227|-19224|-19218|-19212|-19038|-19023|-19018|-19006|-19003|-18996|-18977|-18961|-18952|-18783|-18774|-18773|-18763|-18756|-18741|-18735|-18731|-18722|-18710|-18697|-18696|-18526|-18518|-18501|-18490|-18478|-18463|-18448|-18447|-18446|-18239|-18237|-18231|-18220|-18211|-18201|-18184|-18183|-18181|-18012|-17997|-17988|-17970|-17964|-17961|-17950|-17947|-17931|-17928|-17922|-17759|-17752|-17733|-17730|-17721|-17703|-17701|-17697|-17692|-17683|-17676|-17496|-17487|-17482|-17468|-17454|-17433|-17427|-17417|-17202|-17185|-16983|-16970|-16942|-16915|-16733|-16708|-16706|-16689|-16664|-16657|-16647|-16474|-16470|-16465|-16459|-16452|-16448|-16433|-16429|-16427|-16423|-16419|-16412|-16407|-16403|-16401|-16393|-16220|-16216|-16212|-16205|-16202|-16187|-16180|-16171|-16169|-16158|-16155|-15959|-15958|-15944|-15933|-15920|-15915|-15903|-15889|-15878|-15707|-15701|-15681|-15667|-15661|-15659|-15652|-15640|-15631|-15625|-15454|-15448|-15436|-15435|-15419|-15416|-15408|-15394|-15385|-15377|-15375|-15369|-15363|-15362|-15183|-15180|-15165|-15158|-15153|-15150|-15149|-15144|-15143|-15141|-15140|-15139|-15128|-15121|-15119|-15117|-15110|-15109|-14941|-14937|-14933|-14930|-14929|-14928|-14926|-14922|-14921|-14914|-14908|-14902|-14894|-14889|-14882|-14873|-14871|-14857|-14678|-14674|-14670|-14668|-14663|-14654|-14645|-14630|-14594|-14429|-14407|-14399|-14384|-14379|-14368|-14355|-14353|-14345|-14170|-14159|-14151|-14149|-14145|-14140|-14137|-14135|-14125|-14123|-14122|-14112|-14109|-14099|-14097|-14094|-14092|-14090|-14087|-14083|-13917|-13914|-13910|-13907|-13906|-13905|-13896|-13894|-13878|-13870|-13859|-13847|-13831|-13658|-13611|-13601|-13406|-13404|-13400|-13398|-13395|-13391|-13387|-13383|-13367|-13359|-13356|-13343|-13340|-13329|-13326|-13318|-13147|-13138|-13120|-13107|-13096|-13095|-13091|-13076|-13068|-13063|-13060|-12888|-12875|-12871|-12860|-12858|-12852|-12849|-12838|-12831|-12829|-12812|-12802|-12607|-12597|-12594|-12585|-12556|-12359|-12346|-12320|-12300|-12120|-12099|-12089|-12074|-12067|-12058|-12039|-11867|-11861|-11847|-11831|-11798|-11781|-11604|-11589|-11536|-11358|-11340|-11339|-11324|-11303|-11097|-11077|-11067|-11055|-11052|-11045|-11041|-11038|-11024|-11020|-11019|-11018|-11014|-10838|-10832|-10815|-10800|-10790|-10780|-10764|-10587|-10544|-10533|-10519|-10331|-10329|-10328|-10322|-10315|-10309|-10307|-10296|-10281|-10274|-10270|-10262|-10260|-10256|-10254';
/**
* 获取文字的拼音.
*
* @param string $chinese 中文汉字
* @param bool $onlyfirst 是否只返回拼音首字母
* @param string $delimiter 分隔符
* @param string $charset 文字编码
*
* @return string
*/
public static function get($chinese, $onlyfirst = false, $delimiter = '', $ucfirst = false, $charset = 'utf-8')
{
$keys_a = explode('|', self::$keys);
$values_a = explode('|', self::$values);
$data = array_combine($keys_a, $values_a);
arsort($data);
reset($data);
if ($charset != 'gb2312') {
$chinese = self::_u2_utf8_gb($chinese);
}
$result = '';
for ($i = 0; $i < strlen($chinese); $i++) {
$_P = ord(substr($chinese, $i, 1));
if ($_P > 160) {
$_Q = ord(substr($chinese, ++$i, 1));
$_P = $_P * 256 + $_Q - 65536;
}
$result .= ($onlyfirst ? substr(self::_pinyin($_P, $data), 0, 1) : self::_pinyin($_P, $data));
$result .= $delimiter;
}
if ($delimiter) {
$result = rtrim($result, $delimiter);
}
return preg_replace("/[^a-z0-9_\-]*/i", '', $result);
}
private static function _pinyin($num, $data)
{
if ($num > 0 && $num < 160) {
return chr($num);
} elseif ($num < -20319 || $num > -10247) {
return '';
} else {
foreach ($data as $k => $v) {
if ($v <= $num) {
break;
}
}
return $k;
}
}
private static function _u2_utf8_gb($c)
{
$string = '';
if ($c < 0x80) {
$string .= $c;
} elseif ($c < 0x800) {
$string .= chr(0xC0 | $c >> 6);
$string .= chr(0x80 | $c & 0x3F);
} elseif ($c < 0x10000) {
$string .= chr(0xE0 | $c >> 12);
$string .= chr(0x80 | $c >> 6 & 0x3F);
$string .= chr(0x80 | $c & 0x3F);
} elseif ($c < 0x200000) {
$string .= chr(0xF0 | $c >> 18);
$string .= chr(0x80 | $c >> 12 & 0x3F);
$string .= chr(0x80 | $c >> 6 & 0x3F);
$string .= chr(0x80 | $c & 0x3F);
}
return iconv('UTF-8', 'GB2312//IGNORE', $string);
}
}
+188
View File
@@ -0,0 +1,188 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace ywxapp\utils;
/**
* 随机生成类.
*/
class Random
{
/**
* 生成数字和字母.
*
* @param int $len 长度
*
* @return string
*/
public static function alnum($len = 6)
{
return self::build('alnum', $len);
}
/**
* 仅生成字符.
*
* @param int $len 长度
*
* @return string
*/
public static function alpha($len = 6)
{
return self::build('alpha', $len);
}
/**
* 生成指定长度的随机数字.
*
* @param int $len 长度
*
* @return string
*/
public static function numeric($len = 4)
{
return self::build('numeric', $len);
}
/**
* 数字和字母组合的随机字符串.
*
* @param int $len 长度
*
* @return string
*/
public static function nozero($len = 4)
{
return self::build('nozero', $len);
}
/**
* 能用的随机数生成.
*
* @param string $type 类型 alpha/alnum/numeric/nozero/unique/md5/encrypt/sha1
* @param int $len 长度
*
* @return string
*/
public static function build($type = 'alnum', $len = 8)
{
switch ($type) {
case 'alpha':
case 'alnum':
case 'numeric':
case 'nozero':
switch ($type) {
case 'alpha':
$pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
break;
case 'alnum':
$pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
break;
case 'numeric':
$pool = '0123456789';
break;
case 'nozero':
$pool = '123456789';
break;
}
return substr(str_shuffle(str_repeat($pool, ceil($len / strlen($pool)))), 0, $len);
case 'unique':
case 'md5':
return md5(uniqid(mt_rand()));
case 'encrypt':
case 'sha1':
return sha1(uniqid(mt_rand(), true));
}
}
/**
* 根据数组元素的概率获得键名.
*
* @param array $ps array('p1'=>20, 'p2'=>30, 'p3'=>50);
* @param int $num 默认为1,即随机出来的数量
* @param bool $unique 默认为true,即当num>1时,随机出的数量是否唯一
*
* @return mixed 当num为1时返回键名,反之返回一维数组
*/
public static function lottery($ps, $num = 1, $unique = true)
{
if (!$ps) {
return $num == 1 ? '' : [];
}
if ($num >= count($ps) && $unique) {
$res = array_keys($ps);
return $num == 1 ? $res[0] : $res;
}
$max_exp = 0;
$res = [];
foreach ($ps as $key => $value) {
$value = substr($value, 0, stripos($value, '.') + 6);
$exp = strlen(strstr($value, '.')) - 1;
if ($exp > $max_exp) {
$max_exp = $exp;
}
}
$pow_exp = pow(10, $max_exp);
if ($pow_exp > 1) {
reset($ps);
foreach ($ps as $key => $value) {
$ps[$key] = $value * $pow_exp;
}
}
$pro_sum = array_sum($ps);
if ($pro_sum < 1) {
return $num == 1 ? '' : [];
}
for ($i = 0; $i < $num; $i++) {
$rand_num = mt_rand(1, $pro_sum);
reset($ps);
foreach ($ps as $key => $value) {
if ($rand_num <= $value) {
break;
} else {
$rand_num -= $value;
}
}
if ($num == 1) {
$res = $key;
break;
} else {
$res[$i] = $key;
}
if ($unique) {
$pro_sum -= $value;
unset($ps[$key]);
}
}
return $res;
}
/**
* 获取全球唯一标识.
*
* @return string
*/
public static function uuid()
{
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0xffff)
);
}
}
+193
View File
@@ -0,0 +1,193 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace ywxapp\utils;
/**
* RSA签名类.
*/
class Rsa
{
public $publicKey = '';
public $privateKey = '';
private $_privKey;
/**
* * private key.
*/
private $_pubKey;
/**
* * public key.
*/
private $_keyPath;
/**
* * the keys saving path.
*/
/**
* * the construtor,the param $path is the keys saving path.
*
* @param string $publicKey 公钥
* @param string $privateKey 私钥
*/
public function __construct($publicKey = null, $privateKey = null)
{
$this->setKey($publicKey, $privateKey);
}
/**
* 设置公钥和私钥.
*
* @param string $publicKey 公钥
* @param string $privateKey 私钥
*/
public function setKey($publicKey = null, $privateKey = null)
{
if (!is_null($publicKey)) {
$this->publicKey = $publicKey;
}
if (!is_null($privateKey)) {
$this->privateKey = $privateKey;
}
}
/**
* * setup the private key.
*/
private function setupPrivKey()
{
if (is_resource($this->_privKey)) {
return true;
}
$pem = chunk_split($this->privateKey, 64, "\n");
$pem = "-----BEGIN PRIVATE KEY-----\n" . $pem . "-----END PRIVATE KEY-----\n";
$this->_privKey = openssl_pkey_get_private($pem);
return true;
}
/**
* * setup the public key.
*/
private function setupPubKey()
{
if (is_resource($this->_pubKey)) {
return true;
}
$pem = chunk_split($this->publicKey, 64, "\n");
$pem = "-----BEGIN PUBLIC KEY-----\n" . $pem . "-----END PUBLIC KEY-----\n";
$this->_pubKey = openssl_pkey_get_public($pem);
return true;
}
/**
* * encrypt with the private key.
*/
public function privEncrypt($data)
{
if (!is_string($data)) {
return;
}
$this->setupPrivKey();
$r = openssl_private_encrypt($data, $encrypted, $this->_privKey);
if ($r) {
return base64_encode($encrypted);
}
}
/**
* * decrypt with the private key.
*/
public function privDecrypt($encrypted)
{
if (!is_string($encrypted)) {
return;
}
$this->setupPrivKey();
$encrypted = base64_decode($encrypted);
$r = openssl_private_decrypt($encrypted, $decrypted, $this->_privKey);
if ($r) {
return $decrypted;
}
}
/**
* * encrypt with public key.
*/
public function pubEncrypt($data)
{
if (!is_string($data)) {
return;
}
$this->setupPubKey();
$r = openssl_public_encrypt($data, $encrypted, $this->_pubKey);
if ($r) {
return base64_encode($encrypted);
}
}
/**
* * decrypt with the public key.
*/
public function pubDecrypt($crypted)
{
if (!is_string($crypted)) {
return;
}
$this->setupPubKey();
$crypted = base64_decode($crypted);
$r = openssl_public_decrypt($crypted, $decrypted, $this->_pubKey);
if ($r) {
return $decrypted;
}
}
/**
* 构造签名.
*
* @param string $dataString 被签名数据
*
* @return string
*/
public function sign($dataString)
{
$this->setupPrivKey();
$signature = false;
openssl_sign($dataString, $signature, $this->_privKey);
return base64_encode($signature);
}
/**
* 验证签名.
*
* @param string $dataString 被签名数据
* @param string $signString 已经签名的字符串
*
* @return number 1签名正确 0签名错误
*/
public function verify($dataString, $signString)
{
$this->setupPubKey();
$signature = base64_decode($signString);
$flg = openssl_verify($dataString, $signature, $this->_pubKey);
return $flg;
}
public function __destruct()
{
is_resource($this->_privKey) && @openssl_free_key($this->_privKey);
is_resource($this->_pubKey) && @openssl_free_key($this->_pubKey);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
// server/utils/SignUtil.php
namespace ywxapp\utils;
/**
* SignUtil 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class SignUtil
{
public static function generateSign(array $params, string $secret): string
{
unset($params['sign']);
ksort($params);
$signString = urldecode(http_build_query($params, '', '&'));
return hash_hmac('sha256', $signString, $secret);
}
public static function verifySign(array $params, string $secret): bool
{
if (!isset($params['sign'])) return false;
$sign = $params['sign'];
$expected = self::generateSign($params, $secret);
return strtolower($sign) === strtolower($expected);
}
}
+380
View File
@@ -0,0 +1,380 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace ywxapp\utils;
/**
* StatusCode 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class StatusCode
{
/**
* 这个临时响应表明,迄今为止的所有内容都是可行的,客户端应该继续请求,如果已经完成,则忽略它。
*/
const Continue = 100;
/**
* 该代码是响应客户端的 Upgrade 请求头发送的,指明服务器即将切换的协议。
*/
const SwitchingProtocols = 101;
/**
* 此代码表示服务器已收到并正在处理该请求,但当前没有响应可用。
*/
const Processing = 102;
/**
* 此状态代码主要用于与 Link 链接头一起使用,以允许用户代理在服务器准备响应阶段时开始预加载。
*/
const EarlyHints = 103;
/**
* 请求成功。这是最常见的成功响应。
*/
const Success = 200;
/**
* 在 HTTP 协议中,201 Created 是一个代表成功的应答状态码,表示请求已经被成功处理,并且创建了新的资源。
* 新的资源在应答返回之前已经被创建。
* 同时新增的资源会在应答消息体中返回,其地址或者是原始请求的路径,或者是 Location 首部的值。
*/
const Created = 201;
/**
* 响应状态码 202 Accepted 表示服务器端已经收到请求消息,但是尚未进行处理。
* 但是对于请求的处理却是无保证的,即稍后无法通过 HTTP 协议给客户端发送一个异步请求来告知其请求的处理结果。
* 这个状态码被设计用来将请求交由另外一个进程或者服务器来进行处理,或者是对请求进行批处理的情形。
*/
const Accepted = 202;
/**
* 在 HTTP 协议中,响应状态码 203 Non-Authoritative Information 表示请求已经成功被响应,但是获得的负载与源头服务器的状态码为 200 (OK) 的响应相比,经过了拥有转换功能的 proxy(代理服务器)的修改。
* The 203 状态码有点类似于 Warning 首部的 214Transformation Applied)警告码,后者的优势在于可以应用于任何状态码的响应之中。
*/
const NonAuthoritativeInformation = 203;
/**
* HTTP 204 No Content 成功状态响应码,表示该请求已经成功了,但是客户端客户不需要离开当前页面。
* 默认情况下 204 响应是可缓存的。一个 ETag 标头包含在此类响应中。
*/
const NoContent = 204;
/**
* 在 HTTP 协议中,响应状态码 205 Reset Content 用来通知客户端重置文档视图,比如清空表单内容、重置 canvas 状态或者刷新用户界面。
*/
const ResetContent = 205;
/**
* HTTP 206 Partial Content 成功状态响应代码表示请求已成功,并且主体包含所请求的数据区间,该数据区间是在请求的 Range 首部指定的。
* 如果只包含一个数据区间,那么整个响应的 Content-Type 首部的值为所请求的文件的类型,同时包含 Content-Range 首部。
* 如果包含多个数据区间,那么整个响应的 Content-Type 首部的值为 multipart/byteranges ,其中一个片段对应一个数据区间,并提供 Content-Range 和 Content-Type 描述信息。
*/
const PartialContent = 206;
/**
* HTTP 207 Multi-Status 响应状态码表示可能存在多个响应。
* 响应主体是一个带有 multistatus 根元素的 text/xml 或 application/xml HTTP 实体。XML 主体将列出所有单独的响应状态码。
*/
const MultiStatus = 207;
/**
* Already Reported
*/
const AlreadyReported = 208;
/**
* IM Used
*/
const IMUsed = 226;
/**
* 300 Multiple Choices 是一个用来表示重定向的响应状态码,表示该请求拥有多种可能的响应。
* 用户代理或者用户自身应该从中选择一个。由于没有如何进行选择的标准方法,这个状态码极少使用。
*/
const MultipleChoices = 300;
/**
* HTTP 301 Moved Permanently 说明请求的资源已经被移动到了由 Location 头部指定的 url 上,是固定的不会再改变。搜索引擎会根据该响应修正。
*/
const MovedPermanently = 301;
/**
* HTTP 302 Found 重定向状态码表明请求的资源被暂时的移动到了由该 HTTP 响应的响应头 Location 指定的 URL 上。
* 浏览器会重定向到这个 URL,但是搜索引擎不会对该资源的链接进行更新 (In SEO-speak, it is said that the link-juice is not sent to the new URL)。
*/
const Found = 302;
/**
* HTTP 303 See Other 重定向状态码,通常作为 PUT 或 POST 操作的返回结果,它表示重定向链接指向的不是新上传的资源,而是另外一个页面,比如消息确认页面或上传进度页面。而请求重定向页面的方法要总是使用 GET。
*/
const SeeOther = 303;
/**
* HTTP 304 Not Modified 说明无需再次传输请求的内容,也就是说可以使用缓存的内容。
* 这通常是在一些安全的方法(safe),例如GET 或HEAD 或在请求中附带了头部信息: If-None-Match 或If-Modified-Since。
*/
const NotModified = 304;
/**
* HTTP 307 Temporary Redirect,临时重定向响应状态码,表示请求的资源暂时地被移动到了响应的 Location 首部所指向的 URL 上。
* 原始请求中的请求方法和消息主体会在重定向请求中被重用。在确实需要将重定向请求的方法转换为 GET 的场景下,可以考虑使用 303 See Other 状态码。
* 例如,在使用 PUT 方法进行文件上传操作时,如果需要返回一条确认信息(例如“你已经成功上传了 XYZ”),而不是返回上传的资源本身,就可以使用这个状态码。
* 状态码 307 与 302 之间的唯一区别在于,当发送重定向请求的时候,307 状态码可以确保请求方法和消息主体不会发生变化。
* 如果使用 302 响应状态码,一些旧客户端会错误地将请求方法转换为 GET:也就是说,在 Web 中,如果使用了 GET 以外的请求方法,且返回了 302 状态码,则重定向后的请求方法是不可预测的;
* 但如果使用 307 状态码,之后的请求方法就是可预测的。对于 GET 请求来说,两种情况没有区别。
*/
const TemporaryRedirect = 307;
/**
* 在 HTTP 协议中, 308 Permanent Redirect(永久重定向)是表示重定向的响应状态码,说明请求的资源已经被永久的移动到了由 Location 首部指定的 URL 上。
* 浏览器会进行重定向,同时搜索引擎也会更新其链接(用 SEO 的行话来说,意思是“链接汁”(link juice)被传递到了新的 URL)。
*/
const PermanentRedirect = 308;
/**
* 超文本传输协议(HTTP400 Bad Request 响应状态码表示服务器因某些被认为是客户端错误的原因
* (例如,请求语法错误、无效请求消息格式或者欺骗性请求路由),而无法或不会处理该请求。
*/
const BadRequest = 400;
/**
* 状态码 401 Unauthorized 代表客户端错误,指的是由于缺乏目标资源要求的身份验证凭证,发送的请求未得到满足。
*这个状态码会与 WWW-Authenticate 首部一起发送,其中包含有如何进行验证的信息。
*这个状态类似于 403,但是在该情况下,依然可以进行身份验证。
*/
const Unauthorized = 401;
/**
* 402 Payment Required 是一个被保留使用的非标准客户端错误状态响应码。
*
* 有时,这个状态码表明直到客户端付费之后请求才会被处理。
* 402 状态码被创建最初目的是用于数字现金或微型支付系统,表明客户端请求的内容只有付费之后才能获取。
* 目前还不存在标准的使用约定,不同的实体可以在不同的环境下使用。
*/
const PaymentRequired = 402;
/**
* 状态码 403 Forbidden 代表客户端错误,指的是服务器端有能力处理该请求,但是拒绝授权访问。
*
* 这个状态类似于 401,但进入 403状态后即使重新验证也不会改变该状态。
* 该访问是长期禁止的,并且与应用逻辑密切相关(例如没有足够的权限访问该资源)。
*/
const Forbidden = 403;
/**
* HTTP 响应状态码 404 Not Found 指的是服务器无法找到所请求的资源。
* 返回该响应的链接通常称为坏链(broken link)或死链(dead link),它们会导向链接出错处理(link rot)页面。
*
*404 状态码并不能说明请求的资源是临时还是永久丢失。如果服务器知道该资源是永久丢失,那么应该返回 410(Gone)而不是 404。
*/
const NotFound = 404;
/**
* 状态码 405 Method Not Allowed 表明服务器禁止了使用当前 HTTP 方法的请求。
*/
const MethodNotAllowed = 405;
/**
* HTTP 406 Not Acceptable 客户端错误响应状态码表示服务器无法根据请求的主动内容协商标头中定义的可接受值的列表产生匹配的响应,并且服务器不愿意提供默认表示。
*
*主动内容协商标头包括:
* Accept
* Accept-Encoding
* Accept-Language
*
* 实际上,这种错误极少使用。服务器不应使用此错误代码响应,因为它对终端用户来说很难理解和修复,而是忽略相关的标头并向用户提供实际页面。
*假设即使用户不完全满意,他们也会更喜欢这种情况,而不是错误代码。
*
* 如果服务器返回了这个错误状态码,那么消息体中应该包含所能提供的资源表现形式的列表,允许用户手动进行选择。
*/
const NotAcceptable = 406;
/**
* 状态码 407 Proxy Authentication Required 代表客户端错误,指的是由于缺乏位于浏览器与可以访问所请求资源的服务器之间的代理服务器(proxy server )要求的身份验证凭证,发送的请求尚未得到满足。
*
* 这个状态码会与 Proxy-Authenticate 首部一起发送,其中包含有如何进行验证的信息。
*/
const ProxyAuthenticationRequired = 407;
/**
* 响应状态码 408 Request Timeout 表示服务器想要将没有在使用的连接关闭。一些服务器会在空闲连接上发送此信息,即便是在客户端没有发送任何请求的情况下。
*
* 服务器应该在此类响应中将 Connection 首部的值设置为 "close",因为 408 意味着服务器已经决定将连接关闭,而不是继续等待。
*
* 这类响应出现的比较频繁,源于一些浏览器——例如 Chrome, Firefox 27+, 或者 IE9 等——使用 HTTP 协议中的预连接机制来加速上网体验。同时应该注意到,某些服务器会直接关闭连接,而不发送此类消息。
*/
const RequestTimeout = 408;
/**
* 响应状态码 409 Conflict 表示请求与服务器端目标资源的当前状态相冲突。
*
* 冲突最有可能发生在对 PUT 请求的响应中。例如,当上传文件的版本比服务器上已存在的要旧,从而导致版本冲突的时候,那么就有可能收到状态码为 409 的响应。
*/
const Conflict = 409;
/**
* HTTP 410 Gone 说明请求的目标资源在原服务器上不存在了,并且是永久性的丢失。如果不清楚是否为永久或临时的丢失,应该使用404
*/
const Gone = 410;
/**
* 响应状态码 411 Length Required 属于客户端错误,表示由于缺少确定的Content-Length 首部字段,服务器拒绝客户端的请求。
*
*注意,按照规范,当使用分块模式传输数据的时候, Content-Length 首部是不存在的,但是需要在每一个分块的开始添加该分块的长度,用十六进制数字表示。参见 Transfer-Encoding 获取更多细节信息。
*/
const LengthRequired = 411;
/**
* 在 HTTP 协议中,响应状态码 412 Precondition Failed(先决条件失败)表示客户端错误,意味着对于目标资源的访问请求被拒绝。
* 这通常发生于采用除 GET 和 HEAD 之外的方法进行条件请求时,由首部字段 If-Unmodified-Since 或 If-None-Match 规定的先决条件不成立的情况下。
* 这时候,请求的操作——通常是上传或修改文件——无法执行,从而返回该错误状态码。
*/
const PreconditionFailed = 412;
/**
* HTTP 响应状态码 413 Content Too Large 表示请求主体的大小超过了服务器愿意或有能力处理的限度,服务器可能会关闭连接或返回 Retry-After 标头字段。
*/
const ContentTooLarge = 413;
/**
* 响应码 414 URI Too Long 表示客户端所请求的 URI 超过了服务器允许的范围。
*
* 以下是造成这种罕见情况的几种可能原因:
* 当客户端误将 POST 请求当作 GET 请求时,会带有一个较长的查询字符串 (query);
* 当客户端堕入重定向循环黑洞时,例如,指向自身后缀的重定向 URI 前缀 (a redirected URI prefix that points to a suffix of itself)
* 当客户端对服务器进行攻击,试图寻找潜在的漏洞时。
*/
const URITooLong = 414;
/**
* 415 Unsupported Media Type 是一种 HTTP 协议的错误状态代码,表示服务器由于不支持其有效载荷的格式,从而拒绝接受客户端的请求。
*
* 格式问题的出现有可能源于客户端在 Content-Type 或 Content-Encoding 首部中指定的格式,也可能源于直接对负载数据进行检测的结果。
*/
const UnsupportedMediaType = 415;
/**
* HTTP 416 Range Not Satisfiable 错误状态码意味着服务器无法处理所请求的数据区间。
* 最常见的情况是所请求的数据区间不在文件范围之内,也就是说,Range 首部的值,虽然从语法上来说是没问题的,但是从语义上来说却没有意义。
*
*416 响应报文包含一个 Content-Range 首部,提示无法满足的数据区间(用星号 * 表示),后面紧跟着一个“/”,再后面是当前资源的长度。例如:Content-Range: *\/12777
*
*遇到这一错误状态码时,浏览器一般有两种策略:要么终止操作(例如,一项中断的下载操作被认为是不可恢复的),要么再次请求整个文件。
*/
const RangeNotSatisfiable = 416;
/**
* HTTP 协议中的 417 Expectation Failed 状态码表示客户端错误,意味着服务器无法满足 Expect 请求消息头中的期望条件。
*/
const ExpectationFailed = 417;
/**
* HTTP 418 I'm a teapot 客户端错误响应状态码表示服务器拒绝冲泡咖啡,因为它一直都是茶壶。
* 暂时没有咖啡的组合式咖啡/茶壶应该返回 503。这个错误是对 1998 年和 2014 年愚人节玩笑中定义的超文本咖啡壶控制协议的参考。
*
*一些网站使用这个响应来处理它们不想处理的请求,比如自动查询。
*/
//const UnprocessableEntity = 418;
/**
* HTTP 421 Misdirected Request 客户端错误响应状态码表明,请求被定向到一个无法生成响应的服务器。如果连接被重复使用或选择了其他服务,就有可能出现这种情况。
*/
const MisdirectedRequest = 421;
/**
* HTTP 422 Unprocessable Entity 状态码表示服务器理解请求实体的内容类型,并且请求实体的语法是正确的,但是服务器无法处理所包含的指令。
*/
const UnprocessableEntity = 422;
/**
* HTTP 423 Locked 错误响应状态码表示暂定目标资源被锁定,即无法访问。其内容应包含一些 WebDAV XML 格式的信息。
*/
const Locked = 423;
/**
* HTTP 424 Failed Dependency 客户端错误响应代码表明,由于请求的操作依赖于另一个操作,且该操作失败,因此无法在资源上执行该方法。
*
*普通 web 服务器通常不会返回此状态代码。但其他一些协议,如 WebDAV 可以返回该状态代码。
*例如,在 WebDAV 中,如果发出了 PROPPATCH 请求,其中一条命令失败,那么其他命令也会自动以 424 Failed Dependency 的形式失败。
*/
const FailedDependency = 424;
/**
* 状态码 425 Too Early 代表服务器不愿意冒风险来处理该请求,原因是处理该请求可能会被“重放”,从而造成潜在的重放攻击。
*/
const TooEarly = 425;
/**
* 426 Upgrade Required 是一种 HTTP 协议的错误状态代码,表示服务器拒绝处理客户端使用当前协议发送的请求,但是可以接受其使用升级后的协议发送的请求。
*
* 服务器会在响应中使用 Upgrade 首部来指定要求的协议。
*/
const UpgradeRequired = 426;
/**
* 在 HTTP 协议中,响应状态码 428 Precondition Required 表示服务器端要求发送条件请求。
*
* 一般的,这种情况意味着必要的条件首部——如 If-Match——的缺失。
*
* 当一个条件首部的值不能匹配服务器端的状态的时候,应答的状态码应该是 412 Precondition Failed,前置条件验证失败。
*/
const PreconditionRequired = 428;
/**
* 在 HTTP 协议中,响应状态码 429 Too Many Requests 表示在一定的时间内用户发送了太多的请求,即超出了“频次限制”。
*
* 在响应中,可以提供一个 Retry-After 首部来提示用户需要等待多长时间之后再发送新的请求。
*/
const TooManyRequests = 429;
/**
* 响应码 431 Request Header Fields Too Large 表示由于请求中的首部字段的值过大,服务器拒绝接受客户端的请求。客户端可以在缩减首部字段的体积后再次发送请求。
*
* 该响应码可以用于首部总体体积过大的情况,也可以用于单个首部体积过大的情况。
*
* 这种错误不应该出现于经过良好测试的投入使用的系统当中,而是更多出现于测试新系统的时候
*/
const RequestHeaderFieldsTooLarge = 431;
/**
* 451 Unavailable For Legal Reasons(因法律原因不可用)是一种 HTTP 协议的错误状态代码,表示服务器由于法律原因,无法提供客户端请求的资源,例如可能会导致法律诉讼的页面。
*/
const UnavailableForLegalReasons = 451;
/**
* 在 HTTP 协议中,500 Internal Server Error 是表示服务器端错误的响应状态码,意味着所请求的服务器遇到意外的情况并阻止其执行请求。
*
* 这个错误代码是一个通用的“万能”响应代码。有时候,对于类似于 500 这样的错误,服务器管理员会更加详细地记录相关的请求信息来防止以后同样错误的出现。
*/
const InternalServerError = 500;
/**
* HTTP 501 Not Implemented 服务器错误响应码表示请求的方法不被服务器支持,因此无法被处理。服务器必须支持的方法(即不会返回这个状态码的方法)只有 GET 和 HEAD。
*
* 请注意,你无法修复 501 错误,需要被访问的 web 服务器去修复该问题。
*/
const NotImplemented = 501;
/**
* 502 Bad Gateway 是一种 HTTP 协议的服务端错误状态代码,它表示作为网关或代理的服务器,从上游服务器中接收到的响应是无效的。
*
* 备注: 网关在计算机网络体系中可以指代不同的设备,502 错误通常不是客户端能够修复的,而是需要由途经的 Web 服务器或者代理服务器对其进行修复。
*/
const BadGateway = 502;
/**
* 503 Service Unavailable 是一种 HTTP 协议的服务器端错误状态代码,它表示服务器尚未处于可以接受请求的状态。
*
* 通常造成这种情况的原因是由于服务器停机维护或者已超载。
* 注意在发送该响应的时候,应该同时发送一个对用户友好的页面来解释问题发生的原因。
* 该种响应应该用于临时状况下,与之同时,在可行的情况下,应该在 Retry-After 首部字段中包含服务恢复的预期时间。
*
* 缓存相关的首部在与该响应一同发送时应该小心使用,因为 503 状态码通常应用于临时状况下,而此类响应一般不应该进行缓存。
*/
const ServiceUnavailable = 503;
/**
* 504 Gateway Timeout 是一种 HTTP 协议的服务器端错误状态代码,表示扮演网关或者代理的服务器无法在规定的时间内获得想要的响应。
*
* 网关在计算机网络体系中可以指代不同的设备,504 错误通常不是在客户端可以修复的,而是需要由途径的 Web 服务器或者代理服务器对其进行修复。
*/
const GatewayTimeout = 504;
/**
* 505 HTTP Version Not Supported 是一种 HTTP 协议的服务器端错误状态代码,表示服务器不支持请求所使用的 HTTP 版本。
*/
const HTTPVersionNotSupported = 505;
/**
* HTTP 协议的 506 Variant Also Negotiates 响应状态码 可以在 TCN(透明内容协商,见 RF2295)上下文给出。
* TCN 协议允许客户端取回给定资源的最佳变量/变元,这里服务器支持多个变量/变元。
*
* 506 码表示内部服务器配置错误,其中所选变量/变元自身被配置为参与内容协商,因此并不是合适的协商端点。
*/
const VariantAlsoNegotiates = 506;
/**
* HTTP 协议的 507 Insufficient Storage 响应状态码 可以在 WebDAV 协议(基于 web 的分布式创作和版本控制,参见 RFC 4918)中给出。
*
* 507 码表示服务器不能存储相关内容。准确地说,一个方法可能没有被执行,因为服务器不能存储其表达形式,这里的表达形式指:方法所附带的数据,而且其请求必需已经发送成功。
*/
const InsufficientStorage = 507;
/**
* HTTP 协议的 508 Loop Detected 状态码可以在 WebDAV 协议(基于 Web 的分布式创作和版本控制)中给出。
*
* 508 码表示服务器中断一个操作,因为它在处理具有“Depth: infinity”的请求时遇到了一个无限循环。508 码表示整个操作失败。
*/
const LoopDetected = 508;
/**
* HTTP 协议的 510 Not Extended 响应状态码在 HTTP 扩展框架协议(参见 RFC 2774)中发送。
*
* 在 HTTP 扩展框架协议中,一个客户端可以发送一个包含扩展声明的请求,该声明描述了要使用的扩展。如果服务器接收到这样的请求,但是请求不支持任何所描述的扩展,那么服务器将使用 510 状态码进行响应。
*/
const NotExtended = 510;
/**
* HTTP 511 Network Authentication Required 服务端错误响应状态码表示客户端需要进行认证才能获得网络访问权限。此状态不是由源服务器生成的,而是由控制网络访问的代理服务器拦截生成的。
*
* 网络运营商有时会要求用户进行一些身份验证、接受条款或其他互动,才能授予访问权限(例如在网吧或机场)。他们通常使用客户端的媒体访问控制(MAC)地址来识别尚未完成上述操作的用户。
*/
const NetworkAuthenticationRequired = 511;
}
+87
View File
@@ -0,0 +1,87 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace ywxapp\utils;
/**
* TreeBuilder 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class TreeBuilder {
private $data;
private $sortedData = [];
public function __construct($data) {
$this->data = $data;
}
public function build() {
usort($this->data, function($a, $b) {
return $a['sort'] <=> $b['sort'];
});
$this->buildTree($this->data, 0);
return $this->sortedData;
}
private function buildTree($nodes, $level) {
foreach ($nodes as $node) {
$node['text'] = str_repeat('| ', $level) . $node['title'];
$this->sortedData[] = $node;
if (isset($node['children'])) {
$this->buildTree($node['children'], $level + 1);
}
}
}
private function addChild(&$parent, $child) {
foreach ($parent as &$node) {
if ($node['id'] == $child['pid']) {
if (!isset($node['children'])) {
$node['children'] = [];
}
$node['children'][] = $child;
return;
}
if (isset($node['children'])) {
$this->addChild($node['children'], $child);
}
}
}
function generateCategoryList($categories, $parentId = 0, $level = 0) {
$html = '';
foreach ($categories as $category) {
if ($category['parent_id'] == $parentId) {
$html .= '<ul>';
$html .= '<li style="margin-left: ' . ($level * 20) . 'px;">' . $category['name'];
$html .= generateCategoryList($categories, $category['id'], $level + 1); // 递归调用
$html .= '</li>';
$html .= '</ul>';
}
}
return $html;
}
}
// 示例数据
// $data = [
// ['id' => 1, 'title' => '动态', 'name' => 'news', 'pid' => 0, 'sort' => 2],
// ['id' => 2, 'title' => '测试分类', 'name' => 'test', 'pid' => 0, 'sort' => 1],
// ['id' => 3, 'title' => '官方动态', 'name' => 'gov', 'pid' => 1, 'sort' => 1],
// ];
// $treeBuilder = new TreeBuilder($data);
// $sortedData = $treeBuilder->build();
// print_r($sortedData);
+312
View File
@@ -0,0 +1,312 @@
<?php
declare(strict_types=1);
namespace ywxapp\utils;
use think\App;
use think\route\Url;
/**
* 多根模式 URL 生成
*
* 用法差异(对比 think\app\Url):
* - url('app2:manage/user/read') → 跨应用跳转 /app2/manage/user/read
* - url('manage/user/read') → 当前应用内(自动加 prefix:name 前缀)
* - url('app:admin/user/list') → 强制 /app/admin/user/list
* - url('app2:manage\Blog::read') → 类解析 +跨应用
* - url('[blog_read]') → 仅当前应用(官方限制)
*/
class UrlBuild extends Url
{
/** prefix:name之间的分隔符 */
protected string $prefixSep = ':';
// ============================================================
// 1. 工具方法:prefix解析与反推
// ============================================================
/**
* 从 namespace 反推当前应用的 prefix
* app\manage → 'app'
* app2\manage → 'app2'
*/
protected function getCurrentPrefix(): string
{
$ns = trim($this->app->getNamespace(), '\\');
if ($ns === '') {
return '';
}
$root = explode('\\', $ns)[0] ?? '';
$namespaces = $this->app->config->get('app.app_namespaces', []);
foreach ($namespaces as $key => $nsPrefix) {
if ($root === trim($nsPrefix, '\\')) {
return $key;
}
}
return '';
}
/**
* 获取当前应用的完整名(prefix:name
*/
protected function getCurrentAppName(): string
{
$name = $this->app->http->getName();
$prefix = $this->getCurrentPrefix();
return $prefix !== '' ? ($prefix . $this->prefixSep . $name) : $name;
}
/**
* 拆分 URL 字符串中的 prefix:name/...
* "app2:manage/user/read" → ["app2", "manage/user/read"]
* "manage/user/read" → ["", "manage/user/read"]
* "blog/list" → ["", "blog/list"]
*/
protected function splitPrefix(string $url): array
{
if (!str_contains($url, $this->prefixSep)) {
return ['', $url];
}
$firstSlash = strpos($url, '/');
$firstColon = strpos($url, $this->prefixSep);
// colon 必须在第一段(/之前)
if ($firstSlash === false || $firstColon < $firstSlash) {
return [
substr($url, 0, $firstColon),
substr($url, $firstColon + 1),
];
}
return ['', $url];
}
/**
* 覆盖父类 getAppName(用于 build() 内部拼接)
*官方 think\app\Url 的实现会套 app_map 反向替换 key
* 多根模式: 直接返回 prefix:name 完整形式,反向替换由 parseUrl 自己处理
*/
protected function getAppName(): string
{
return $this->getCurrentAppName();
}
// ============================================================
// 2. 核心:parseUrl 重写
// ============================================================
/**
* 直接解析 URL 地址
*/
protected function parseUrl(string $url, string|bool &$domain): string
{
$request = $this->app->request;
// (1) 以 / 开头 → 直接作为路由地址
if (str_starts_with($url, '/')) {
return substr($url, 1);
}
// (2) 包含 \\ → 解析到类
// 支持: app2:manage\Blog\Article::read
if (str_contains($url, '\\')) {
return ltrim(str_replace('\\', '/', $url), '/');
}
// (3) 以 @ 开头 → 解析到控制器
if (str_starts_with($url, '@')) {
return substr($url, 1);
}
// (4) 空 URL → 当前 controller/action
if ($url === '') {
$url = $request->controller() . '/' . $request->action();
if (!$this->app->http->isBind()) {
$url = $this->getAppName() . '/' . $url;
}
return $url;
}
// ====== 多根模式特有逻辑 ======
// 拆分 prefix:name
[$prefix, $rest] = $this->splitPrefix($url);
$effectiveUrl = $prefix !== '' ? $rest : $url;
// 拆分 controller/action/app
$controller = $request->controller();
$path = explode('/', $effectiveUrl);
$action = array_pop($path);
$controller = empty($path) ? $controller : array_pop($path);
// 应用名
if ($prefix !== '') {
// 跨应用: prefix 已指定,path 最后一段就是应用名
$appName = empty($path) ? $this->getCurrentAppName() : array_pop($path);
} else {
// 当前应用内
$appName = empty($path) ? $this->getCurrentAppName() : array_pop($path);
}
$url = $controller . '/' . $action;
// ====== 域名绑定处理 ======
$bind = $this->app->config->get('app.domain_bind', []);
if ($prefix !== '') {
// 跨应用跳转 → 找目标应用绑定的域名
$targetApp = $prefix . $this->prefixSep . $appName;
if ($key = array_search($targetApp, $bind)) {
// 用户没显式传 domain 时,才用绑定域名
$domain = $domain ?: $key;
}
} elseif (!$this->app->http->isBind()) {
// 当前应用内 → 检查当前应用是否绑定域名
$currentApp = $this->app->http->getName();
if ($key = array_search($currentApp, $bind)) {
// 当前域名就是绑定的 → 强制使用
if (isset($bind[$_SERVER['SERVER_NAME'] ?? ''])) {
$domain = $_SERVER['SERVER_NAME'];
}
$domain = is_bool($domain) ? $key : $domain;
} else {
// 非绑定模式,处理 app_map
$map = $this->app->config->get('app.app_map', []);
if ($key = array_search($appName, $map)) {
// 反向映射: app_name → URL段
$url = $key . '/' . $url;
} else {
$url = $appName . '/' . $url;
}
}
}
return $url;
}
// ============================================================
// 3. build() 重写:处理跨应用域名拼接
// ============================================================
public function build(): string
{
$url = $this->url;
$suffix = $this->suffix;
$domain = $this->domain;
$request = $this->app->request;
$vars = $this->vars;
// [name] 路由名
if (str_starts_with($url, '[') && $pos = strpos($url, ']')) {
$name = substr($url, 1, $pos - 1);
$url = 'name' . substr($url, $pos + 1);
}
if (!str_contains($url, '://') && !str_starts_with($url, '/')) {
$info = parse_url($url);
$url = !empty($info['path']) ? $info['path'] : '';
if (isset($info['fragment'])) {
$anchor = $info['fragment'];
if (str_contains($anchor, '?')) {
[$anchor, $info['query']] = explode('?', $anchor, 2);
}
if (str_contains($anchor, '@')) {
[$anchor, $domain] = explode('@', $anchor, 2);
}
} elseif (str_contains($url, '@') && !str_contains($url, '\\')) {
[$url, $domain] = explode('@', $url, 2);
}
}
if ($url) {
$checkName = $name ?? $url . (isset($info['query']) ? '?' . $info['query'] : '');
$checkDomain = $domain && is_string($domain) ? $domain : null;
$rule = $this->route->getName($checkName, $checkDomain);
if (empty($rule) && isset($info['query'])) {
$rule = $this->route->getName($url, $checkDomain);
parse_str($info['query'], $params);
$vars = array_merge($params, $vars);
unset($info['query']);
}
}
if (!empty($rule) && $match = $this->getRuleUrl($rule, $vars, $domain)) {
// 路由名命中
$url = $match[0];
if ($domain && !empty($match[1])) {
$domain = $match[1];
}
if (!is_null($match[2])) {
$suffix = $match[2];
}
// 未绑定域名 → 加当前应用前缀
if (!$this->app->http->isBind()) {
$url = $this->getAppName() . '/' . $url;
}
} elseif (!empty($rule) && isset($name)) {
throw new \InvalidArgumentException('route name not exists:' . $name);
} else {
// URL 绑定
$bind = (string) $this->route->getDomainBind($domain && is_string($domain) ? $domain : null);
if ($bind && str_starts_with($url, $bind)) {
$url = substr($url, strlen($bind) + 1);
}
$url = $this->parseUrl($url, $domain);
if (isset($info['query'])) {
parse_str($info['query'], $params);
$vars = array_merge($params, $vars);
}
}
// 还原分隔符
$depr = $this->route->config('pathinfo_depr');
$url = str_replace('/', $depr, $url);
$file = $request->baseFile();
if ($file && !str_starts_with($request->url(), $file)) {
$file = str_replace('\\', '/', dirname($file));
}
$url = rtrim($file, '/') . '/' . $url;
// 后缀
if (str_ends_with($url, '/') || '' == $url) {
$suffix = '';
} else {
$suffix = $this->parseSuffix($suffix);
}
// 锚点
$anchor = !empty($anchor) ? '#' . $anchor : '';
// 参数
if (!empty($vars)) {
if ($this->route->config('url_common_param')) {
$vars = http_build_query($vars);
$url .= $suffix . ($vars ? '?' . $vars : '') . $anchor;
} else {
foreach ($vars as $var => $val) {
$val = (string) $val;
if ('' !== $val) {
$url .= $depr . $var . $depr . urlencode($val);
}
}
$url .= $suffix . $anchor;
}
} else {
$url .= $suffix . $anchor;
}
// 域名(★ 多根模式下,parseDomain 会按当前域名处理)
$domain = $this->parseDomain($url, $domain);
return $domain . rtrim($this->root, '/') . '/' . ltrim($url, '/');
}
/**
* parseDomain 重写:跨应用跳转时,使用 parseUrl 已选定的 $domain
*官方实现依赖 Route::getDomains,多根场景下不够灵活
*/
protected function parseDomain(string &$url, string|bool $domain): string
{
// parseUrl 已经根据 domain_bind 选好了 $domain,这里直接信任
return parent::parseDomain($url, $domain);
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace ywxapp\utils\storage;
use OSS\OssClient;
use OSS\Core\OssException;
use think\file\UploadedFile;
/**
* AliOssStorage 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class AliOssStorage implements StorageInterface
{
protected $config;
protected $ossClient;
public function __construct(array $config)
{
$this->config = $config;
$this->ossClient = new OssClient(
$config['access_key_id'],
$config['access_key_secret'],
$config['endpoint']
);
}
public function upload(UploadedFile $file, string $path, array $options = []): array
{
try {
$this->ossClient->uploadFile(
$this->config['bucket'],
$path,
$file->getRealPath()
);
return [
'url' => $this->getUrl($path),
'path' => $path,
'storage' => 'alioss'
];
} catch (OssException $e) {
throw new \Exception('阿里云上传失败: ' . $e->getMessage());
}
}
public function delete(string $path): bool
{
try {
$this->ossClient->deleteObject($this->config['bucket'], $path);
return true;
} catch (\Exception $e) {
return false;
}
}
public function getUrl(string $path): string
{
// 如果配置了CDN域名,优先使用
if (!empty($this->config['cdn_domain'])) {
return rtrim($this->config['cdn_domain'], '/') . '/' . ltrim($path, '/');
}
// 否则使用OSS默认域名
return "https://{$this->config['bucket']}.{$this->config['endpoint']}/{$path}";
}
public function exists(string $path): bool
{
try {
$this->ossClient->getObjectMeta($this->config['bucket'], $path);
return true;
} catch (\Exception $e) {
return false;
}
}
}
+82
View File
@@ -0,0 +1,82 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace ywxapp\utils\storage;
use think\file\UploadedFile;
/**
* LocalStorage 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class LocalStorage implements StorageInterface
{
protected $config;
public function __construct(array $config)
{
$this->config = $config;
}
/**
* 解析存储根目录(支持绝对路径与相对 public 目录的相对路径)
*/
protected function getRoot(): string
{
$root = $this->config['root'] ?? 'storage';
// 绝对路径(Windows 盘符 或 Unix 根目录)直接使用,不再拼接 public_path
if (preg_match('#^[a-zA-Z]:[\\\\/]|^/#', $root)) {
return rtrim($root, '/\\');
}
return rtrim(public_path() . ltrim($root, '/\\'), '/\\');
}
public function upload(UploadedFile $file, string $path, array $options = []): array
{
// 移动文件到指定目录
$root = $this->getRoot();
$url = $this->config['url'] ?? '/storage';
$dir = $root . DIRECTORY_SEPARATOR . ltrim($path, '/\\');
if (! is_dir($dir)) {
mkdir($dir, 0777, true);
}
$ext = $file->extension() ?: pathinfo($file->getOriginalName(), PATHINFO_EXTENSION);
$savename = date('His') . '_' . uniqid() . '.' . $ext;
$file->move($dir, $savename);
$realName = $file->getSaveName() ?: $savename;
$relative = ltrim($path, '/\\') . '/' . $realName;
return [$realName, $relative, rtrim($url, '/') . '/' . $relative];
}
public function delete(string $path): bool
{
$fullPath = $this->getRoot() . DIRECTORY_SEPARATOR . ltrim($path, '/\\');
return file_exists($fullPath) && unlink($fullPath);
}
public function getUrl(string $path): string
{
$domain = $this->config['domain'] ?? '';
return rtrim($domain, '/') . '/' . ltrim($path, '/');
}
public function exists(string $path): bool
{
$fullPath = $this->getRoot() . DIRECTORY_SEPARATOR . ltrim($path, '/\\');
return file_exists($fullPath);
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace ywxapp\utils\storage;
use think\file\UploadedFile;
/**
* 存储驱动统一接口
* 定义了所有云存储平台必须实现的方法
*/
interface StorageInterface
{
/**
* 上传文件
* @param UploadedFile $file 上传的文件对象
* @param string $path 存储路径
* @param array $options 上传选项
* @return array 包含url、path等信息的结果集
* @throws \Exception
*/
public function upload(UploadedFile $file, string $path, array $options = []): array;
/**
* 删除文件
* @param string $path 文件路径
* @return bool
*/
public function delete(string $path): bool;
/**
* 获取文件访问URL
* @param string $path 文件路径
* @return string
*/
public function getUrl(string $path): string;
/**
* 判断文件是否存在
* @param string $path
* @return bool
*/
public function exists(string $path): bool;
}