初始化本地仓库

This commit is contained in:
2026-08-31 11:33:59 +08:00
commit 663f74b633
2390 changed files with 402918 additions and 0 deletions
+330
View File
@@ -0,0 +1,330 @@
<?php
declare(strict_types=1);
namespace core\tool;
use app\service\admin\system\ConfigService as SystemConfigService;
use app\attribute\EncryptResponse;
use app\attribute\SkipApiEncrypt;
use madong\swagger\helper\AnnotationHelper;
use support\Container;
use support\Context;
use support\Request;
use Webman\Http\Response;
class ApiEncryptService
{
public const CONTEXT_AES_KEY = 'api_encrypt.aes_key';
public const CONTEXT_AES_IV = 'api_encrypt.aes_iv';
public const CONTEXT_ACTIVE = 'api_encrypt.active';
public function isEnabled(): bool
{
return (bool) $this->getConfigValue('enabled', false);
}
public function getScope(): string
{
return (string) $this->getConfigValue('scope', 'off');
}
public function getKeyVersion(): string
{
return (string) $this->getConfigValue('key_version', 'v1');
}
public function getAlgorithm(): string
{
return (string) $this->getConfigValue('algorithm', 'rsa-aes');
}
public function getAesMode(): string
{
return strtoupper((string) $this->getConfigValue('aes_mode', 'AES-256-CBC'));
}
public function getPublicKey(): string
{
$path = (string) $this->getConfigValue('public_key_path', '');
return $path !== '' && is_file($path) ? (string) file_get_contents($path) : '';
}
public function getPrivateKey(): string
{
$path = (string) $this->getConfigValue('private_key_path', '');
return $path !== '' && is_file($path) ? (string) file_get_contents($path) : '';
}
public function isJsonRequest(Request $request): bool
{
$contentType = strtolower((string) $request->header('content-type', ''));
return str_contains($contentType, 'application/json');
}
public function isJsonResponse(Response $response): bool
{
$contentType = strtolower((string) ($response->getHeader('Content-Type') ?: ''));
return str_contains($contentType, 'application/json');
}
public function shouldExcludePath(string $path): bool
{
$path = '/' . ltrim($path, '/');
$excludePaths = (array) $this->getConfigValue('exclude_paths', []);
foreach ($excludePaths as $excludePath) {
$excludePath = '/' . ltrim((string) $excludePath, '/');
if ($excludePath === $path || str_starts_with($path, $excludePath . '/')) {
return true;
}
}
return false;
}
public function shouldEncrypt(Request $request): bool
{
if (!$this->isEnabled()) {
return false;
}
$scope = $this->getScope();
if ($scope === 'off') {
return false;
}
$path = $request->path();
if ($this->shouldExcludePath($path)) {
return false;
}
$controllerClass = $request->controller;
$action = $request->action;
if (!$controllerClass || !$action) {
return false;
}
if (AnnotationHelper::hasMethodAnnotation($controllerClass, $action, SkipApiEncrypt::class)
|| AnnotationHelper::hasClassAnnotation($controllerClass, SkipApiEncrypt::class)) {
return false;
}
if ($scope === 'all') {
return true;
}
$encryptedPaths = (array) $this->getConfigValue('encrypted_paths', []);
if ($this->matchesEncryptedPath($request->path(), $encryptedPaths)) {
return true;
}
return AnnotationHelper::hasMethodAnnotation($controllerClass, $action, EncryptResponse::class)
|| AnnotationHelper::hasClassAnnotation($controllerClass, EncryptResponse::class);
}
public function getEncryptedPaths(): array
{
$paths = (array) $this->getConfigValue('encrypted_paths', []);
return array_values(array_filter(array_map(static function ($path) {
return '/' . ltrim((string) $path, '/');
}, $paths)));
}
public function isEncryptedRequest(Request $request): bool
{
return (string) $request->header('x-api-encrypt', '') === '1';
}
public function decryptRequest(Request $request): array
{
$encryptedKey = trim((string) $request->header('x-api-encrypt-key', ''));
$encryptedIv = trim((string) $request->header('x-api-encrypt-iv', ''));
if ($encryptedKey === '' || $encryptedIv === '') {
throw new \RuntimeException('缺少加密请求头');
}
$privateKey = $this->getPrivateKey();
if ($privateKey === '') {
throw new \RuntimeException('接口加密私钥未配置');
}
$aesKey = RSAService::decryptOaep($encryptedKey, $privateKey);
$iv = RSAService::decryptOaep($encryptedIv, $privateKey);
if ($aesKey === '' || $iv === '') {
throw new \RuntimeException('会话密钥解密失败');
}
Context::set(self::CONTEXT_AES_KEY, $aesKey);
Context::set(self::CONTEXT_AES_IV, $iv);
Context::set(self::CONTEXT_ACTIVE, true);
$body = trim($request->rawBody());
if ($body === '') {
return [];
}
$payload = json_decode($body, true);
if (!is_array($payload)) {
throw new \RuntimeException('加密请求体格式错误');
}
if (($payload['encrypted'] ?? false) !== true) {
return $payload;
}
$cipherText = (string) ($payload['data'] ?? '');
if ($cipherText === '') {
throw new \RuntimeException('缺少加密数据');
}
$plain = $this->aesDecrypt($cipherText, $aesKey, $iv);
$data = json_decode($plain, true);
if (!is_array($data)) {
throw new \RuntimeException('解密后的请求数据格式错误');
}
return $data;
}
public function encryptResponseBody(string $body): string
{
$aesKey = (string) Context::get(self::CONTEXT_AES_KEY, '');
$iv = (string) Context::get(self::CONTEXT_AES_IV, '');
if ($aesKey === '' || $iv === '') {
throw new \RuntimeException('缺少响应加密会话信息');
}
$cipherText = $this->aesEncrypt($body, $aesKey, $iv);
return json_encode([
'encrypted' => true,
'algorithm' => $this->getAlgorithm(),
'key_version' => $this->getKeyVersion(),
'data' => $cipherText,
], JSON_UNESCAPED_UNICODE);
}
public function applyDecryptedPayload(Request $request, array $data): void
{
if ($data === []) {
return;
}
if ($request->method() === 'GET') {
$request->setGet(array_merge($request->get(), $data));
return;
}
$request->setPost($data);
}
public function hasActiveSessionKey(): bool
{
return (bool) Context::get(self::CONTEXT_ACTIVE, false);
}
public function generateAesKey(): string
{
return random_bytes(32);
}
public function generateIv(): string
{
return random_bytes(16);
}
public function aesEncrypt(string $plainText, string $aesKey, string $iv): string
{
$cipher = openssl_encrypt($plainText, 'AES-256-CBC', $aesKey, OPENSSL_RAW_DATA, $iv);
if ($cipher === false) {
throw new \RuntimeException('AES 加密失败');
}
return base64_encode($cipher);
}
public function aesDecrypt(string $cipherText, string $aesKey, string $iv): string
{
$decoded = base64_decode($cipherText, true);
if ($decoded === false) {
throw new \RuntimeException('AES 密文解码失败');
}
$plain = openssl_decrypt($decoded, 'AES-256-CBC', $aesKey, OPENSSL_RAW_DATA, $iv);
if ($plain === false) {
throw new \RuntimeException('AES 解密失败');
}
return $plain;
}
public function generateKeyPair(int $bits = 2048): array
{
return RSAService::generateKeys($bits);
}
protected function getConfigValue(string $key, mixed $default = null): mixed
{
$fileConfig = config('api_encrypt', []);
$dbConfig = $this->getDatabaseConfig();
if (array_key_exists($key, $dbConfig) && $dbConfig[$key] !== null && $dbConfig[$key] !== '') {
return $dbConfig[$key];
}
return $fileConfig[$key] ?? $default;
}
protected function getDatabaseConfig(): array
{
try {
/** @var SystemConfigService $service */
$service = Container::get(SystemConfigService::class);
$config = $service->config('api_encrypt_setting', [], ['group_code' => 'system']);
return is_array($config) ? $config : [];
} catch (\Throwable) {
return [];
}
}
protected function matchesEncryptedPath(string $path, array $encryptedPaths): bool
{
$path = '/' . ltrim($path, '/');
foreach ($encryptedPaths as $encryptedPath) {
$encryptedPath = '/' . ltrim((string) $encryptedPath, '/');
if ($encryptedPath === $path || str_starts_with($path, $encryptedPath . '/')) {
return true;
}
}
return false;
}
public function persistKeyPair(array $keys): void
{
$publicPath = (string) $this->getConfigValue('public_key_path', '');
$privatePath = (string) $this->getConfigValue('private_key_path', '');
if ($publicPath === '' || $privatePath === '') {
throw new \RuntimeException('接口加密密钥路径未配置');
}
$publicDir = dirname($publicPath);
$privateDir = dirname($privatePath);
if (!is_dir($publicDir)) {
mkdir($publicDir, 0777, true);
}
if (!is_dir($privateDir)) {
mkdir($privateDir, 0777, true);
}
file_put_contents($publicPath, (string) ($keys['public'] ?? ''));
file_put_contents($privatePath, (string) ($keys['private'] ?? ''));
}
}
+224
View File
@@ -0,0 +1,224 @@
<?php
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: http://www.madong.tech
*/
namespace core\tool;
use core\exception\handler\AdminException;
/**
* 断言辅助类
*/
final class AssertHelper
{
/**
* 断言表达式为 true
*
* @param mixed $expression 要检查的值(会自动转换为bool)
* @param string $message 自定义错误消息
*
* @throws \core\exception\handler\AdminException() 如果表达式不为true
*/
public static function isTrue(mixed $expression, string $message = "[Assertion failed] - Expected value to be true"): void
{
if (!filter_var($expression, FILTER_VALIDATE_BOOL)) {
throw new AdminException($message);
}
}
/**
* 断言表达式为 false
*
* @throws \core\exception\handler\AdminException
*/
public static function isFalse(mixed $expression, string $message = "[Assertion failed] - Expected value to be false"): void
{
if (filter_var($expression, FILTER_VALIDATE_BOOL)) {
throw new AdminException($message);
}
}
/**
* 断言值为 null
*
* @throws \core\exception\handler\AdminException
*/
public static function isNull(mixed $value, string $message = "[Assertion failed] - Expected value to be null"): void
{
if ($value !== null) {
throw new AdminException($message);
}
}
/**
* 断言值不为 null
*
* @throws \core\exception\handler\AdminException
*/
public static function notNull(mixed $value, string $message = "[Assertion failed] - Value cannot be null"): void
{
if ($value === null) {
throw new AdminException($message);
}
}
/**
* 断言字符串非空(不为null且不为空字符串)
*
* @throws \core\exception\handler\AdminException
*/
public static function notEmpty(?string $value, string $message = "[Assertion failed] - Value cannot be empty"): void
{
if ($value === null || trim($value) === '') {
throw new AdminException($message);
}
}
/**
* 断言数组非空
*
* @throws \core\exception\handler\AdminException
*/
public static function notEmptyArray(array $array, string $message = "[Assertion failed] - Array cannot be empty"): void
{
if (empty($array)) {
throw new AdminException($message);
}
}
/**
* 断言值为指定类型
*
* @throws \core\exception\handler\AdminException
*/
public static function isInstanceOf(mixed $value, string $className, string $message = null): void
{
if (!($value instanceof $className)) {
$message = $message ?? sprintf("[Assertion failed] - Expected instance of %s", $className);
throw new AdminException($message);
}
}
/**
* 断言值为数字或数字字符串
*
* @throws \core\exception\handler\AdminException
*/
public static function isNumeric(mixed $value, string $message = "[Assertion failed] - Expected a numeric value"): void
{
if (!is_numeric($value)) {
throw new AdminException($message);
}
}
/**
* 断言值在指定范围内
*
* @throws \core\exception\handler\AdminException
*/
public static function inRange(mixed $value, mixed $min, mixed $max, string $message = null): void
{
if ($value < $min || $value > $max) {
$message = $message ?? sprintf("[Assertion failed] - Value must be between %s and %s", $min, $max);
throw new AdminException($message);
}
}
/**
* 断言字符串匹配正则表达式
*
* @throws \core\exception\handler\AdminException
*/
public static function matchesRegex(string $value, string $pattern, string $message = null): void
{
if (!preg_match($pattern, $value)) {
$message = $message ?? sprintf("[Assertion failed] - Value does not match pattern %s", $pattern);
throw new AdminException($message);
}
}
/**
* 断言两个值相等(松散比较 ==)
*
* @throws \core\exception\handler\AdminException
*/
public static function equals(mixed $actual, mixed $expected, string $message = null): void
{
if ($actual != $expected) {
$message = $message ?? sprintf("[Assertion failed] - Expected %s, got %s", var_export($expected, true), var_export($actual, true));
throw new AdminException($message);
}
}
/**
* 断言两个值严格相等(===)
*
* @throws \core\exception\handler\AdminException
*/
public static function strictEquals(mixed $actual, mixed $expected, string $message = null): void
{
if ($actual !== $expected) {
$message = $message ?? sprintf("[Assertion failed] - Expected strict %s, got %s", var_export($expected, true), var_export($actual, true));
throw new AdminException($message);
}
}
/**
* 断言值为有效的电子邮件格式
*
* @throws \core\exception\handler\AdminException
*/
public static function isEmail(string $value, string $message = "[Assertion failed] - Value is not a valid email address"): void
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new AdminException($message);
}
}
/**
* 断言值为有效的URL格式
*
* @throws \core\exception\handler\AdminException
*/
public static function isUrl(string $value, string $message = "[Assertion failed] - Value is not a valid URL"): void
{
if (!filter_var($value, FILTER_VALIDATE_URL)) {
throw new AdminException($message);
}
}
/**
* 断言目录存在
*
* @throws \core\exception\handler\AdminException
*/
public static function directoryExists(string $path, string $message = null): void
{
if (!is_dir($path)) {
$message = $message ?? sprintf("[Assertion failed] - Directory %s does not exist", $path);
throw new AdminException($message);
}
}
/**
* 断言文件存在
*
* @throws \core\exception\handler\AdminException
*/
public static function fileExists(string $path, string $message = null): void
{
if (!file_exists($path)) {
$message = $message ?? sprintf("[Assertion failed] - File %s does not exist", $path);
throw new AdminException($message);
}
}
}
+105
View File
@@ -0,0 +1,105 @@
<?php
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: http://www.madong.tech
*/
namespace core\tool;
use support\Response;
class Json
{
private static int $code = 200;
public static function make(int $code, string $msg, mixed $data = null, ?array $replace = []): Response
{
$msg = __($msg);
$res = compact('code', 'msg');
if (!is_null($data))
$res['data'] = $data;
if (is_numeric($res['msg'])) {
$res['code'] = $res['msg'];
$res['msg'] = $res['msg'];
}
$defaultHttpCode = self::$code;
// 如果 code 不是 -1 或 400,设置 HTTP 状态码
if (!in_array($code, [-1, 0, 200, 400]) && $code >= 100 && $code < 600) {
$defaultHttpCode = $code;
}
return new Response($defaultHttpCode, ['Content-Type' => 'application/json'], json_encode($res));
}
public static function success(string|array $msg = 'common.operation.success', mixed $data = [], ?array $replace = []): Response
{
if (is_array($msg)) {
$data = $msg;
$msg = 'common.operation.success';
}
if (is_array($data)) {
$data = self::convertLongNumbersToString($data);
}
return self::make(0, $msg, $data, $replace);
}
public static function fail(string|array $msg = 'common.operation.failed', mixed $data = null, int|string $code = -1, ?array $replace = []): Response
{
if (is_array($msg)) {
$data = $msg;
$msg = 'common.operation.failed';
}
return self::make($code, $msg, $data, $replace);
}
public static function status($status, $msg, $result = []): Response
{
$status = strtoupper($status);
if (is_array($msg)) {
$result = $msg;
$msg = 'common.operation.success';
}
return self::success($msg, compact('status', 'result'));
}
/**
* 数据输出雪花id 转字符串
*
* @param $array
*
* @return mixed
*/
public static function convertLongNumbersToString($array): mixed
{
foreach ($array as &$value) {
if (is_array($value)) {
// 如果值是数组,递归调用
$value = self::convertLongNumbersToString($value);
} elseif (is_object($value)) {
// 如果值是对象,检查是否是模型对象
if (method_exists($value, 'toArray')) {
// 将模型对象转换为数组并递归处理
$value = self::convertLongNumbersToString($value->toArray());
}
} elseif (is_numeric($value) && strlen((string)$value) > 15) {
// 如果是数字且长度大于 15,转换为字符串
$value = (string)$value;
}
}
return $array;
}
}
+147
View File
@@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: https://madong.tech
*/
namespace core\tool;
use Illuminate\Database\Eloquent\Relations\Relation;
/**
* 多态映射辅助类
*
* 提供便捷的多态映射操作方法
*
* @author Mr.April
* @since 1.0
*/
class MorphMapHelper
{
/**
* 获取所有已注册的多态映射
*
* @return array
*/
public static function getAllMaps(): array
{
return Relation::morphMap();
}
/**
* 获取指定别名对应的模型类
*
* @param string $alias
* @return string|null
*/
public static function getModelClass(string $alias): ?string
{
return Relation::getMorphedModel($alias);
}
/**
* 获取指定模型类对应的别名
*
* @param string $modelClass
* @return string|null
*/
public static function getModelAlias(string $modelClass): ?string
{
$maps = self::getAllMaps();
$flipped = array_flip($maps);
return $flipped[$modelClass] ?? null;
}
/**
* 检查别名是否已注册
*
* @param string $alias
* @return bool
*/
public static function hasAlias(string $alias): bool
{
return array_key_exists($alias, self::getAllMaps());
}
/**
* 检查模型类是否已注册
*
* @param string $modelClass
* @return bool
*/
public static function hasModelClass(string $modelClass): bool
{
return in_array($modelClass, self::getAllMaps());
}
/**
* 格式化输出映射信息(用于调试)
*
* @return string
*/
public static function debugInfo(): string
{
$maps = self::getAllMaps();
if (empty($maps)) {
return "No morph maps registered.\n";
}
$output = "Morph Maps (Total: " . count($maps) . "):\n";
$output .= str_repeat('-', 80) . "\n";
foreach ($maps as $alias => $modelClass) {
$exists = class_exists($modelClass);
$status = $exists ? '✓' : '✗';
$output .= sprintf(
" %-20s => %-60s %s\n",
$alias,
$modelClass,
$status
);
}
$output .= str_repeat('-', 80) . "\n";
$output .= "Legend: ✓ = class exists, ✗ = class not found\n";
return $output;
}
/**
* 根据模型类自动生成别名(用于开发时生成配置)
*
* @param string $modelClass
* @return string
*/
public static function generateAlias(string $modelClass): string
{
// 移除命名空间前缀
$className = basename(str_replace('\\', '/', $modelClass));
// 转换为小写并用下划线连接
// 例如: Question -> question, MemberWithdraw -> member_withdraw
$alias = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $className));
return $alias;
}
/**
* 批量添加映射(用于动态注册)
*
* @param array $maps
* @return void
*/
public static function addMaps(array $maps): void
{
$existingMaps = self::getAllMaps();
$newMaps = array_merge($existingMaps, $maps);
Relation::enforceMorphMap($newMaps);
}
}
+155
View File
@@ -0,0 +1,155 @@
<?php
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: https://madong.tech
*/
namespace core\tool;
use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Crypt\RSA;
/**
* RSA秘钥服务
*
* @author Mr.April
* @since 1.0
*/
class RSAService
{
/**
* 生成秘钥对
*
* @param int $bits
*
* @return array
*/
public static function generateKeys(int $bits = 2048): array
{
$privateKey = RSA::createKey($bits);
$privatePem = $privateKey->toString('PKCS1');
$publicPem = $privateKey->getPublicKey()->toString('PKCS8');
return [
'private' => $privatePem,
'public' => $publicPem,
];
}
/**
* 签名
*
* @param string $data
* @param string $privatePem
*
* @return string
*/
public static function sign(string $data, string $privatePem): string
{
$privateKey = PublicKeyLoader::loadPrivateKey($privatePem)
->withHash('sha256')
->withPadding(RSA::SIGNATURE_PKCS1);
return $privateKey->sign($data);
}
/**
* 验证签名
*
* @param string $data
* @param string $signature
* @param string $publicPem
*
* @return bool
*/
public static function verify(string $data, string $signature, string $publicPem): bool
{
$publicKey = PublicKeyLoader::load($publicPem)
->withHash('sha256')
->withPadding(RSA::SIGNATURE_PKCS1);
return $publicKey->verify($data, $signature);
}
/**
* 公钥加密
*
* @param string $data
* @param string $publicPem
*
* @return string
*/
public static function encrypt(string $data, string $publicPem): string
{
$publicKey = PublicKeyLoader::load($publicPem)->withPadding(RSA::ENCRYPTION_PKCS1);
return base64_encode($publicKey->encrypt($data));
}
/**
* 私钥解密
*
* @param string $cipherBase64
* @param string $privatePem
*
* @return string
*/
public static function decrypt(string $cipherBase64, string $privatePem): string
{
$privateKey = PublicKeyLoader::loadPrivateKey($privatePem)
->withPadding(RSA::ENCRYPTION_PKCS1);
$cipher = base64_decode($cipherBase64);
return $privateKey->decrypt($cipher);
}
public static function encryptOaep(string $data, string $publicPem): string
{
$publicKey = PublicKeyLoader::load($publicPem)
->withHash('sha256')
->withMGFHash('sha256')
->withPadding(RSA::ENCRYPTION_OAEP);
return base64_encode($publicKey->encrypt($data));
}
public static function decryptOaep(string $cipherBase64, string $privatePem): string
{
$privateKey = PublicKeyLoader::loadPrivateKey($privatePem)
->withHash('sha256')
->withMGFHash('sha256')
->withPadding(RSA::ENCRYPTION_OAEP);
$cipher = base64_decode($cipherBase64);
return $privateKey->decrypt($cipher);
}
/**
* 公钥掩码显示
*
* @param string $pem
* @param int $keep
*
* @return string
*/
public static function maskKey(string $pem, int $keep = 20): string
{
$clean = str_replace(
["\r", "\n", "-----BEGIN PUBLIC KEY-----", "-----END PUBLIC KEY-----"],
'',
$pem
);
$len = strlen($clean);
return substr($clean, 0, $keep) . '...' . substr($clean, -$keep);
}
}
+291
View File
@@ -0,0 +1,291 @@
<?php
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: http://www.madong.tech
*/
namespace core\tool;
use core\uuid\UUIDGenerator;
class Sse
{
/**
* 创建SSE响应字符串
*
* @param mixed $data 数据内容
* @param string $event 事件类型
* @param string|null $uuid 消息UUID,用于防止消息错乱
* @param array $extend 扩展数据
* @param string|null $key 标识键
* @return string
* @throws \Exception
*/
public static function make(mixed $data, string $event = 'message', ?string $uuid = null, array $extend = [], ?string $key = null): string
{
// 如果没有提供uuid,自动生成一个
if (empty($uuid)) {
$uuid = UUIDGenerator::generate();
}
$dataPayload = [
'data' => $data,
'event' => $event,
'uuid' => $uuid,
'extend' => $extend,
'key' => $key,
'date' => date('Y-m-d H:i:s'),
];
$jsonData = json_encode($dataPayload, JSON_UNESCAPED_UNICODE);
if ($jsonData === false) {
$jsonData = json_encode(['error' => 'JSON encode error'], JSON_UNESCAPED_UNICODE);
}
$sseLines = [];
$sseLines[] = "id: $uuid";
$sseLines[] = "event: $event";
$sseLines[] = "data: $jsonData";
return implode("\n", $sseLines) . "\n\n";
}
/**
* 创建成功事件SSE响应字符串
*
* @param string $message 消息内容
* @param mixed|null $data 附加数据
* @param string|null $uuid 消息UUID
* @param array $extend 扩展数据
* @param string|null $key 标识键
* @return string
* @throws \Exception
*/
public static function success(string $message, mixed $data = null, ?string $uuid = null, array $extend = [], ?string $key = null): string
{
return self::make([
'status' => 'success',
'message' => $message,
'data' => $data,
], 'success', $uuid, $extend, $key);
}
/**
* 创建错误事件SSE响应字符串(连接错误,会导致连接中断)
*
* @param string $message 错误消息
* @param mixed|null $data 附加数据
* @param string|null $uuid 消息UUID
* @param array $extend 扩展数据
* @param string|null $key 标识键
* @return string
* @throws \Exception
*/
public static function error(string $message, mixed $data = null, ?string $uuid = null, array $extend = [], ?string $key = null): string
{
return self::make([
'status' => 'error',
'message' => $message,
'data' => $data,
'error_type' => 'connection', // 连接错误类型
], 'error', $uuid, $extend, $key);
}
/**
* 创建运行错误事件SSE响应字符串(不会导致连接中断)
*
* @param string $message 错误消息
* @param mixed|null $data 附加数据
* @param string|null $uuid 消息UUID
* @param array $extend 扩展数据
* @param string|null $key 标识键
* @return string
* @throws \Exception
*/
public static function runtimeError(string $message, mixed $data = null, ?string $uuid = null, array $extend = [], ?string $key = null): string
{
return self::make([
'status' => 'error',
'message' => $message,
'data' => $data,
'error_type' => 'runtime', // 运行错误类型,不会中断连接
], 'runtime_error', $uuid, $extend, $key);
}
/**
* 创建警告事件SSE响应字符串(不会导致连接中断)
*
* @param string $message 警告消息
* @param mixed|null $data 附加数据
* @param string|null $uuid 消息UUID
* @param array $extend 扩展数据
* @param string|null $key 标识键
* @return string
* @throws \Exception
*/
public static function warning(string $message, mixed $data = null, ?string $uuid = null, array $extend = [], ?string $key = null): string
{
return self::make([
'status' => 'warning',
'message' => $message,
'data' => $data,
], 'warning', $uuid, $extend, $key);
}
/**
* 创建进度事件SSE响应字符串
*
* @param string $message 消息内容
* @param int $progress 进度百分比
* @param mixed|null $data 附加数据
* @param string|null $uuid 消息UUID
* @param array $extend 扩展数据
* @param string|null $key 标识键
* @return string
* @throws \Exception
*/
public static function progress(string $message, int $progress, mixed $data = null, ?string $uuid = null, array $extend = [], ?string $key = null): string
{
return self::make([
'status' => 'progress',
'message' => $message,
'progress' => $progress,
'data' => $data,
], 'progress', $uuid, $extend, $key);
}
/**
* 创建完成事件SSE响应字符串
*
* @param string $message 消息内容
* @param mixed|null $data 附加数据
* @param string|null $uuid 消息UUID
* @param array $extend 扩展数据
* @param string|null $key 标识键
* @return string
* @throws \Exception
*/
public static function completed(string $message, mixed $data = null, ?string $uuid = null, array $extend = [], ?string $key = null): string
{
return self::make([
'status' => 'completed',
'message' => $message,
'data' => $data,
], 'completed', $uuid, $extend, $key);
}
/**
* 创建心跳事件SSE响应字符串(用于保持连接)
*
* @param string|null $uuid 消息UUID
* @return string
* @throws \Exception
*/
public static function heartbeat(?string $uuid = null): string
{
return self::make([
'status' => 'heartbeat',
'message' => 'keep alive',
], 'heartbeat', $uuid);
}
/**
* 创建重试事件SSE响应字符串(用于设置重连时间)
*
* @param int $retryMs 重试时间(毫秒)
* @param string|null $uuid 消息UUID
* @return string
* @throws \Exception
*/
public static function retry(int $retryMs = 3000, ?string $uuid = null): string
{
$dataPayload = [
'status' => 'retry',
'retry_ms' => $retryMs,
];
$jsonData = json_encode($dataPayload, JSON_UNESCAPED_UNICODE);
if ($jsonData === false) {
$jsonData = json_encode(['error' => 'JSON encode error'], JSON_UNESCAPED_UNICODE);
}
$sseLines = [];
$sseLines[] = "id: " . ($uuid ?: UUIDGenerator::generate());
$sseLines[] = "event: retry";
$sseLines[] = "retry: $retryMs";
$sseLines[] = "data: $jsonData";
return implode("\n", $sseLines) . "\n\n";
}
/**
* 发送SSE响应头
*
* @param bool $closeConnection 是否关闭连接
*/
public static function sendHeaders(bool $closeConnection = false): void
{
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: ' . ($closeConnection ? 'close' : 'keep-alive'));
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: X-Requested-With, Content-Type');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
// 禁用输出缓冲
if (ob_get_level()) {
ob_end_flush();
}
// 立即发送头部
flush();
}
/**
* 检查连接是否仍然有效
*
* @return bool
*/
public static function isConnectionAlive(): bool
{
return connection_status() === CONNECTION_NORMAL;
}
/**
* 格式化SSE数据(用于Terminal类)
*
* @param string $data 数据内容
* @param string|null $event 事件类型
* @param string|null $uuid 消息UUID
* @param array $extend 扩展数据
*
* @return array
* @throws \Exception
*/
public static function format(string $data, ?string $event = null, ?string $uuid = null, array $extend = []): array
{
if (empty($uuid)) {
$uuid = UUIDGenerator::generate();
}
// 如果没有提供事件类型,使用默认值
if (empty($event)) {
$event = 'message';
}
return [
'data' => $data,
'event' => $event,
'uuid' => $uuid,
'extend' => $extend,
'date' => date('Y-m-d H:i:s'),
];
}
}
+410
View File
@@ -0,0 +1,410 @@
<?php
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: https://madong.tech
*/
namespace core\tool;
use app\process\Monitor;
use core\utils\Exception;
use Throwable;
use Workerman\Timer;
use Workerman\Worker;
class Util
{
/**
* 密码哈希
*
* @param $password
* @param string $algo
*
* @return false|string|null
*/
public static function passwordHash($password, string $algo = PASSWORD_DEFAULT)
{
return password_hash($password, $algo);
}
/**
* 验证密码哈希
*
* @param string $password
* @param string $hash
*
* @return bool
*/
public static function passwordVerify(string $password, string $hash): bool
{
return password_verify($password, $hash);
}
/**
* 获取语义化时间
*
* @param $time
*
* @return false|string
*/
public static function humanDate($time)
{
$timestamp = is_numeric($time) ? $time : strtotime($time);
$dur = time() - $timestamp;
if ($dur < 0) {
return date('Y-m-d', $timestamp);
} else {
if ($dur < 60) {
return $dur . '秒前';
} else {
if ($dur < 3600) {
return floor($dur / 60) . '分钟前';
} else {
if ($dur < 86400) {
return floor($dur / 3600) . '小时前';
} else {
if ($dur < 2592000) { // 30天内
return floor($dur / 86400) . '天前';
} else {
return date('Y-m-d', $timestamp);;
}
}
}
}
}
return date('Y-m-d', $timestamp);
}
/**
* 格式化文件大小
*
* @param $file_size
*
* @return string
*/
public static function formatBytes($file_size): string
{
$size = sprintf("%u", $file_size);
if ($size == 0) {
return ("0 Bytes");
}
$size_name = array(" Bytes", " KB", " MB", " GB", " TB", " PB", " EB", " ZB", " YB");
return round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . $size_name[$i];
}
/**
* 数据库字符串转义
*
* @param $var
*
* @return false|string
*/
public static function pdoQuote($var)
{
return Util::db()->getPdo()->quote($var);
}
/**
* 检查表名是否合法
*
* @param string $table
*
* @return string
* @throws Exception
*/
public static function checkTableName(string $table): string
{
if (!preg_match('/^[a-zA-Z_0-9]+$/', $table)) {
throw new Exception('表名不合法');
}
return $table;
}
/**
* 变量或数组中的元素只能是字母数字下划线组合
*
* @param $var
*
* @return mixed
* @throws Exception
*/
public static function filterAlphaNum($var)
{
$vars = (array)$var;
array_walk_recursive($vars, function ($item) {
if (is_string($item) && !preg_match('/^[a-zA-Z_0-9]+$/', $item)) {
throw new Exception('参数不合法');
}
});
return $var;
}
/**
* 变量或数组中的元素只能是字母数字
*
* @param $var
*
* @return mixed
* @throws Exception
*/
public static function filterNum($var)
{
$vars = (array)$var;
array_walk_recursive($vars, function ($item) {
if (is_string($item) && !preg_match('/^[0-9]+$/', $item)) {
throw new Exception('参数不合法');
}
});
return $var;
}
/**
* @desc 检测是否是合法URL Path
*
* @param $var
*
* @return string
* @throws Exception
*/
public static function filterUrlPath($var): string
{
if (!is_string($var)) {
throw new Exception('参数不合法,地址必须是一个字符串!');
}
if (strpos($var, 'https://') === 0 || strpos($var, 'http://') === 0) {
if (!filter_var($var, FILTER_VALIDATE_URL)) {
throw new Exception('参数不合法,不是合法的URL地址!');
}
} elseif (!preg_match('/^[a-zA-Z0-9_\-\/&?.]+$/', $var)) {
throw new Exception('参数不合法,不是合法的Path!');
}
return $var;
}
/**
* 检测是否是合法Path
*
* @param $var
*
* @return string
* @throws \Exception
*/
public static function filterPath($var): string
{
if (!is_string($var) || !preg_match('/^[a-zA-Z0-9_\-\/]+$/', $var)) {
throw new \Exception('参数不合法');
}
return $var;
}
/**
* 类转换为url path
*
* @param $controller_class
*
* @return false|string
*/
static function controllerToUrlPath($controller_class): false|string
{
$key = strtolower($controller_class);
$action = '';
if (strpos($key, '@')) {
[$key, $action] = explode('@', $key, 2);
}
$prefix = 'plugin';
$paths = explode('\\', $key);
if (count($paths) < 2) {
return false;
}
$base = '';
if (str_starts_with($key, "$prefix\\")) {
if (count($paths) < 4) {
return false;
}
array_shift($paths);
$plugin = array_shift($paths);
$base = "/app/$plugin/";
}
array_shift($paths);
foreach ($paths as $index => $path) {
if ($path === 'controller') {
unset($paths[$index]);
}
}
$suffix = 'controller';
$code = $base . implode('/', $paths);
if (str_ends_with($code, $suffix)) {
$code = substr($code, 0, -strlen($suffix));
}
return $action ? "$code/$action" : $code;
}
/**
* 转换为驼峰
*
* @param string $value
*
* @return string
*/
public static function camel(string $value): string
{
static $cache = [];
$key = $value;
if (isset($cache[$key])) {
return $cache[$key];
}
$value = ucwords(str_replace(['-', '_'], ' ', $value));
return $cache[$key] = str_replace(' ', '', $value);
}
/**
* 转换为小驼峰
*
* @param $value
*
* @return string
*/
public static function smCamel($value): string
{
return lcfirst(static::camel($value));
}
/**
* 获取某个composer包的版本
*
* @param string $package
*
* @return mixed|string
*/
public static function getPackageVersion(string $package): mixed
{
$installed_php = base_path('vendor/composer/installed.php');
if (is_file($installed_php)) {
$packages = include $installed_php;
}
return substr($packages['versions'][$package]['version'] ?? 'unknown ', 0, -2);
}
/**
* Reload webman
*
* @return bool
*/
public static function reloadWebman(): bool
{
if (function_exists('posix_kill')) {
try {
posix_kill(posix_getppid(), SIGUSR1);
return true;
} catch (Throwable $e) {
}
} else {
Timer::add(1, function () {
Worker::stopAll();
});
}
return false;
}
/**
* Pause file monitor
*
* @return void
*/
public static function pauseFileMonitor(): void
{
if (method_exists(Monitor::class, 'pause')) {
Monitor::pause();
}
}
/**
* Resume file monitor
*
* @return void
*/
public static function resumeFileMonitor(): void
{
if (method_exists(Monitor::class, 'resume')) {
Monitor::resume();
}
}
/**
* 加载指定插件的所有菜单位置
*
* @param string $pluginName 插件名称
* @param array $menuTypes 菜单类型列表,默认加载admin和site
*
* @return array 插件的菜单位置配置
*/
public static function loadPluginMenus(string $pluginName, array $menuTypes = ['admin', 'site']): array
{
$menus = [];
$pluginPath = base_path('plugin' . DIRECTORY_SEPARATOR);
foreach ($menuTypes as $menuType) {
$menuFilePath = $pluginPath . $pluginName . DIRECTORY_SEPARATOR
. 'resource' . DIRECTORY_SEPARATOR
. 'menu' . DIRECTORY_SEPARATOR
. $menuType . '.php';
if (is_file($menuFilePath)) {
try {
$menuConfig = include $menuFilePath;
if (is_array($menuConfig)) {
$menus[$menuType] = $menuConfig;
}
} catch (\Throwable $e) {
// 记录错误但不中断程序
}
}
}
return $menus;
}
/**
* 加载指定插件的特定类型菜单
*
* @param string $pluginName 插件名称
* @param string $menuType 菜单类型(admin 或 site)
*
* @return array 插件的菜单位置配置
*/
public static function loadPluginMenu(string $pluginName, string $menuType): array
{
$pluginPath = base_path('plugin' . DIRECTORY_SEPARATOR);
$menuFilePath = $pluginPath . $pluginName . DIRECTORY_SEPARATOR
. 'resource' . DIRECTORY_SEPARATOR
. 'menu' . DIRECTORY_SEPARATOR
. $menuType . '.php';
if (!is_file($menuFilePath)) {
return [];
}
try {
$menuConfig = include $menuFilePath;
return is_array($menuConfig) ? $menuConfig : [];
} catch (\Throwable $e) {
// 记录错误但不中断程序
return [];
}
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: https://madong.tech
*/
return [
'enable' => true,
];