初始化本地仓库
This commit is contained in:
@@ -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'] ?? ''));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
Reference in New Issue
Block a user