数据中心同步
This commit is contained in:
137
Moncter/app/utils/ApiResponseHelper.php
Normal file
137
Moncter/app/utils/ApiResponseHelper.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace app\utils;
|
||||
|
||||
/**
|
||||
* API 响应辅助工具类
|
||||
*
|
||||
* 提供统一的 API 响应格式
|
||||
*/
|
||||
class ApiResponseHelper
|
||||
{
|
||||
/**
|
||||
* 判断是否为开发环境
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected static function isDevelopment(): bool
|
||||
{
|
||||
return config('app.debug', false) || env('APP_ENV', 'production') === 'development';
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应
|
||||
*
|
||||
* @param mixed $data 响应数据
|
||||
* @param string $message 响应消息
|
||||
* @param int $httpCode HTTP状态码
|
||||
* @return \support\Response
|
||||
*/
|
||||
public static function success($data = null, string $message = 'ok', int $httpCode = 200): \support\Response
|
||||
{
|
||||
$response = [
|
||||
'code' => 0,
|
||||
'msg' => $message,
|
||||
];
|
||||
|
||||
if ($data !== null) {
|
||||
$response['data'] = $data;
|
||||
}
|
||||
|
||||
return json($response, $httpCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误响应
|
||||
*
|
||||
* @param string $message 错误消息
|
||||
* @param int $code 错误码(业务错误码,非HTTP状态码)
|
||||
* @param int $httpCode HTTP状态码
|
||||
* @param array<string, mixed> $extra 额外信息
|
||||
* @return \support\Response
|
||||
*/
|
||||
public static function error(
|
||||
string $message,
|
||||
int $code = 400,
|
||||
int $httpCode = 400,
|
||||
array $extra = []
|
||||
): \support\Response {
|
||||
$response = [
|
||||
'code' => $code,
|
||||
'msg' => $message,
|
||||
];
|
||||
|
||||
// 开发环境可以返回更多调试信息
|
||||
if (self::isDevelopment() && !empty($extra)) {
|
||||
$response = array_merge($response, $extra);
|
||||
}
|
||||
|
||||
return json($response, $httpCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常响应
|
||||
*
|
||||
* @param \Throwable $exception 异常对象
|
||||
* @param int $httpCode HTTP状态码
|
||||
* @return \support\Response
|
||||
*/
|
||||
public static function exception(\Throwable $exception, int $httpCode = 500): \support\Response
|
||||
{
|
||||
// 记录错误日志
|
||||
LoggerHelper::logError($exception);
|
||||
|
||||
$code = 500;
|
||||
$message = '内部服务器错误';
|
||||
|
||||
// 根据异常类型设置错误码和消息
|
||||
if ($exception instanceof \InvalidArgumentException) {
|
||||
$code = 400;
|
||||
$message = $exception->getMessage();
|
||||
} elseif ($exception instanceof \RuntimeException) {
|
||||
$code = 500;
|
||||
$message = $exception->getMessage();
|
||||
}
|
||||
|
||||
$response = [
|
||||
'code' => $code,
|
||||
'msg' => $message,
|
||||
];
|
||||
|
||||
// 开发环境返回详细错误信息
|
||||
if (self::isDevelopment()) {
|
||||
$response['debug'] = [
|
||||
'message' => $exception->getMessage(),
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
'trace' => $exception->getTraceAsString(),
|
||||
];
|
||||
}
|
||||
|
||||
return json($response, $httpCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证错误响应
|
||||
*
|
||||
* @param array<string, string> $errors 验证错误列表
|
||||
* @return \support\Response
|
||||
*/
|
||||
public static function validationError(array $errors): \support\Response
|
||||
{
|
||||
$message = '参数验证失败';
|
||||
if (!empty($errors)) {
|
||||
$firstError = reset($errors);
|
||||
$message = is_array($firstError) ? $firstError[0] : $firstError;
|
||||
}
|
||||
|
||||
$response = [
|
||||
'code' => 400,
|
||||
'msg' => $message,
|
||||
'errors' => $errors,
|
||||
];
|
||||
|
||||
return json($response, 400);
|
||||
}
|
||||
}
|
||||
|
||||
143
Moncter/app/utils/DataMaskingHelper.php
Normal file
143
Moncter/app/utils/DataMaskingHelper.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace app\utils;
|
||||
|
||||
/**
|
||||
* 数据脱敏工具类
|
||||
*
|
||||
* 用于在接口返回和日志中脱敏敏感信息
|
||||
*/
|
||||
class DataMaskingHelper
|
||||
{
|
||||
/**
|
||||
* 脱敏身份证号
|
||||
*
|
||||
* @param string|null $idCard 身份证号
|
||||
* @return string 脱敏后的身份证号(如:110101********1234)
|
||||
*/
|
||||
public static function maskIdCard(?string $idCard): string
|
||||
{
|
||||
if (empty($idCard)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$config = config('encryption.masking.id_card', []);
|
||||
$prefixLength = $config['prefix_length'] ?? 6;
|
||||
$suffixLength = $config['suffix_length'] ?? 4;
|
||||
$maskChar = $config['mask_char'] ?? '*';
|
||||
|
||||
$length = mb_strlen($idCard);
|
||||
if ($length <= $prefixLength + $suffixLength) {
|
||||
// 如果长度不足以脱敏,返回全部用*替代
|
||||
return str_repeat($maskChar, $length);
|
||||
}
|
||||
|
||||
$prefix = mb_substr($idCard, 0, $prefixLength);
|
||||
$suffix = mb_substr($idCard, -$suffixLength);
|
||||
$maskLength = $length - $prefixLength - $suffixLength;
|
||||
|
||||
return $prefix . str_repeat($maskChar, $maskLength) . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏手机号
|
||||
*
|
||||
* @param string|null $phone 手机号
|
||||
* @return string 脱敏后的手机号(如:138****5678)
|
||||
*/
|
||||
public static function maskPhone(?string $phone): string
|
||||
{
|
||||
if (empty($phone)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$config = config('encryption.masking.phone', []);
|
||||
$prefixLength = $config['prefix_length'] ?? 3;
|
||||
$suffixLength = $config['suffix_length'] ?? 4;
|
||||
$maskChar = $config['mask_char'] ?? '*';
|
||||
|
||||
$length = mb_strlen($phone);
|
||||
if ($length <= $prefixLength + $suffixLength) {
|
||||
return str_repeat($maskChar, $length);
|
||||
}
|
||||
|
||||
$prefix = mb_substr($phone, 0, $prefixLength);
|
||||
$suffix = mb_substr($phone, -$suffixLength);
|
||||
$maskLength = $length - $prefixLength - $suffixLength;
|
||||
|
||||
return $prefix . str_repeat($maskChar, $maskLength) . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏邮箱
|
||||
*
|
||||
* @param string|null $email 邮箱
|
||||
* @return string 脱敏后的邮箱(如:ab***@example.com)
|
||||
*/
|
||||
public static function maskEmail(?string $email): string
|
||||
{
|
||||
if (empty($email)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$config = config('encryption.masking.email', []);
|
||||
$prefixLength = $config['prefix_length'] ?? 2;
|
||||
$maskChar = $config['mask_char'] ?? '*';
|
||||
|
||||
$atPos = mb_strpos($email, '@');
|
||||
if ($atPos === false) {
|
||||
// 如果没有@符号,按普通字符串处理
|
||||
$length = mb_strlen($email);
|
||||
if ($length <= $prefixLength) {
|
||||
return str_repeat($maskChar, $length);
|
||||
}
|
||||
$prefix = mb_substr($email, 0, $prefixLength);
|
||||
return $prefix . str_repeat($maskChar, $length - $prefixLength);
|
||||
}
|
||||
|
||||
$localPart = mb_substr($email, 0, $atPos);
|
||||
$domain = mb_substr($email, $atPos);
|
||||
|
||||
$localLength = mb_strlen($localPart);
|
||||
if ($localLength <= $prefixLength) {
|
||||
$maskedLocal = str_repeat($maskChar, $localLength);
|
||||
} else {
|
||||
$prefix = mb_substr($localPart, 0, $prefixLength);
|
||||
$maskedLocal = $prefix . str_repeat($maskChar, $localLength - $prefixLength);
|
||||
}
|
||||
|
||||
return $maskedLocal . $domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏数组中的敏感字段
|
||||
*
|
||||
* @param array<string, mixed> $data 数据数组
|
||||
* @param array<string> $sensitiveFields 敏感字段列表(如:['id_card', 'phone', 'email'])
|
||||
* @return array<string, mixed> 脱敏后的数组
|
||||
*/
|
||||
public static function maskArray(array $data, array $sensitiveFields = ['id_card', 'id_card_encrypted', 'phone', 'email']): array
|
||||
{
|
||||
$masked = $data;
|
||||
|
||||
foreach ($sensitiveFields as $field) {
|
||||
if (isset($masked[$field]) && is_string($masked[$field])) {
|
||||
switch ($field) {
|
||||
case 'id_card':
|
||||
case 'id_card_encrypted':
|
||||
$masked[$field] = self::maskIdCard($masked[$field]);
|
||||
break;
|
||||
case 'phone':
|
||||
$masked[$field] = self::maskPhone($masked[$field]);
|
||||
break;
|
||||
case 'email':
|
||||
$masked[$field] = self::maskEmail($masked[$field]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $masked;
|
||||
}
|
||||
}
|
||||
|
||||
141
Moncter/app/utils/EncryptionHelper.php
Normal file
141
Moncter/app/utils/EncryptionHelper.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace app\utils;
|
||||
|
||||
/**
|
||||
* 加密工具类
|
||||
*
|
||||
* 提供身份证等敏感数据的加密、解密和哈希功能
|
||||
*/
|
||||
class EncryptionHelper
|
||||
{
|
||||
/**
|
||||
* 加密字符串(使用 AES-256-CBC)
|
||||
*
|
||||
* @param string $plaintext 明文
|
||||
* @return string 加密后的密文(base64编码,包含IV)
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public static function encrypt(string $plaintext): string
|
||||
{
|
||||
if (empty($plaintext)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$config = config('encryption', []);
|
||||
$keyString = $config['aes']['key'] ?? '';
|
||||
$cipher = $config['aes']['cipher'] ?? 'AES-256-CBC';
|
||||
$ivLength = $config['aes']['iv_length'] ?? 16;
|
||||
|
||||
if (empty($keyString)) {
|
||||
throw new \InvalidArgumentException('加密密钥配置错误,密钥不能为空');
|
||||
}
|
||||
|
||||
// 使用 SHA256 哈希处理密钥,确保密钥长度为32字节(AES-256需要256位密钥)
|
||||
// 即使原始密钥长度不够,哈希后也会得到固定长度的密钥
|
||||
$key = substr(hash('sha256', $keyString), 0, 32);
|
||||
|
||||
// 生成随机IV
|
||||
$iv = openssl_random_pseudo_bytes($ivLength);
|
||||
if ($iv === false) {
|
||||
throw new \RuntimeException('无法生成随机IV');
|
||||
}
|
||||
|
||||
// 加密
|
||||
$encrypted = openssl_encrypt($plaintext, $cipher, $key, OPENSSL_RAW_DATA, $iv);
|
||||
if ($encrypted === false) {
|
||||
throw new \RuntimeException('加密失败: ' . openssl_error_string());
|
||||
}
|
||||
|
||||
// 将IV和密文组合,然后base64编码
|
||||
return base64_encode($iv . $encrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密字符串
|
||||
*
|
||||
* @param string $ciphertext 密文(base64编码,包含IV)
|
||||
* @return string 解密后的明文
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public static function decrypt(string $ciphertext): string
|
||||
{
|
||||
if (empty($ciphertext)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$config = config('encryption', []);
|
||||
$keyString = $config['aes']['key'] ?? '';
|
||||
$cipher = $config['aes']['cipher'] ?? 'AES-256-CBC';
|
||||
$ivLength = $config['aes']['iv_length'] ?? 16;
|
||||
|
||||
if (empty($keyString)) {
|
||||
throw new \InvalidArgumentException('加密密钥配置错误,密钥不能为空');
|
||||
}
|
||||
|
||||
// 使用 SHA256 哈希处理密钥,确保密钥长度为32字节
|
||||
// 即使原始密钥长度不够,哈希后也会得到固定长度的密钥
|
||||
$key = substr(hash('sha256', $keyString), 0, 32);
|
||||
|
||||
// 解码base64
|
||||
$data = base64_decode($ciphertext, true);
|
||||
if ($data === false) {
|
||||
throw new \RuntimeException('密文格式错误(base64解码失败)');
|
||||
}
|
||||
|
||||
// 提取IV和密文
|
||||
if (strlen($data) < $ivLength) {
|
||||
throw new \RuntimeException('密文格式错误(长度不足)');
|
||||
}
|
||||
|
||||
$iv = substr($data, 0, $ivLength);
|
||||
$encrypted = substr($data, $ivLength);
|
||||
|
||||
// 解密
|
||||
$decrypted = openssl_decrypt($encrypted, $cipher, $key, OPENSSL_RAW_DATA, $iv);
|
||||
if ($decrypted === false) {
|
||||
throw new \RuntimeException('解密失败: ' . openssl_error_string());
|
||||
}
|
||||
|
||||
return $decrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算字符串的哈希值(用于身份证匹配)
|
||||
*
|
||||
* @param string $plaintext 明文
|
||||
* @return string 哈希值(hex编码)
|
||||
*/
|
||||
public static function hash(string $plaintext): string
|
||||
{
|
||||
if (empty($plaintext)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$config = config('encryption', []);
|
||||
$algorithm = $config['hash']['algorithm'] ?? 'sha256';
|
||||
$useSalt = $config['hash']['use_salt'] ?? false;
|
||||
$salt = $config['hash']['salt'] ?? '';
|
||||
|
||||
$data = $plaintext;
|
||||
if ($useSalt && !empty($salt)) {
|
||||
$data = $salt . $plaintext;
|
||||
}
|
||||
|
||||
return hash($algorithm, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证明文是否匹配哈希值
|
||||
*
|
||||
* @param string $plaintext 明文
|
||||
* @param string $hash 哈希值
|
||||
* @return bool
|
||||
*/
|
||||
public static function verifyHash(string $plaintext, string $hash): bool
|
||||
{
|
||||
$calculatedHash = self::hash($plaintext);
|
||||
return hash_equals($calculatedHash, $hash);
|
||||
}
|
||||
}
|
||||
|
||||
102
Moncter/app/utils/IdCardHelper.php
Normal file
102
Moncter/app/utils/IdCardHelper.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace app\utils;
|
||||
|
||||
/**
|
||||
* 身份证工具类
|
||||
*
|
||||
* 职责:
|
||||
* - 从身份证号中提取出生日期
|
||||
* - 从身份证号中提取性别
|
||||
* - 验证身份证号格式
|
||||
*/
|
||||
class IdCardHelper
|
||||
{
|
||||
/**
|
||||
* 从身份证号中提取出生日期
|
||||
*
|
||||
* @param string $idCard 身份证号(15位或18位)
|
||||
* @return \DateTimeImmutable|null 出生日期,解析失败返回null
|
||||
*/
|
||||
public static function extractBirthday(string $idCard): ?\DateTimeImmutable
|
||||
{
|
||||
$idCard = trim($idCard);
|
||||
$length = strlen($idCard);
|
||||
|
||||
if ($length === 18) {
|
||||
// 18位身份证:第7-14位是出生日期(YYYYMMDD)
|
||||
$birthDateStr = substr($idCard, 6, 8);
|
||||
$year = (int)substr($birthDateStr, 0, 4);
|
||||
$month = (int)substr($birthDateStr, 4, 2);
|
||||
$day = (int)substr($birthDateStr, 6, 2);
|
||||
} elseif ($length === 15) {
|
||||
// 15位身份证:第7-12位是出生日期(YYMMDD)
|
||||
$birthDateStr = substr($idCard, 6, 6);
|
||||
$year = (int)substr($birthDateStr, 0, 2);
|
||||
$month = (int)substr($birthDateStr, 2, 2);
|
||||
$day = (int)substr($birthDateStr, 4, 2);
|
||||
|
||||
// 15位身份证的年份需要加上1900或2000
|
||||
// 通常出生年份在1900-2000之间,如果大于当前年份的后两位,则加1900,否则加2000
|
||||
$currentYearLastTwo = (int)date('y');
|
||||
if ($year > $currentYearLastTwo) {
|
||||
$year += 1900;
|
||||
} else {
|
||||
$year += 2000;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证日期是否有效
|
||||
if (!checkdate($month, $day, $year)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new \DateTimeImmutable(sprintf('%04d-%02d-%02d', $year, $month, $day));
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从身份证号中提取性别
|
||||
*
|
||||
* @param string $idCard 身份证号(15位或18位)
|
||||
* @return int 性别:1=男,2=女,0=未知
|
||||
*/
|
||||
public static function extractGender(string $idCard): int
|
||||
{
|
||||
$idCard = trim($idCard);
|
||||
$length = strlen($idCard);
|
||||
|
||||
if ($length === 18) {
|
||||
// 18位身份证:第17位(索引16)是性别码
|
||||
$genderCode = (int)substr($idCard, 16, 1);
|
||||
} elseif ($length === 15) {
|
||||
// 15位身份证:第15位(索引14)是性别码
|
||||
$genderCode = (int)substr($idCard, 14, 1);
|
||||
} else {
|
||||
return 0; // 未知
|
||||
}
|
||||
|
||||
// 奇数表示男性,偶数表示女性
|
||||
return ($genderCode % 2 === 1) ? 1 : 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从身份证号中提取所有可解析的信息
|
||||
*
|
||||
* @param string $idCard 身份证号
|
||||
* @return array<string, mixed> 包含 birthday 和 gender 的数组
|
||||
*/
|
||||
public static function extractInfo(string $idCard): array
|
||||
{
|
||||
return [
|
||||
'birthday' => self::extractBirthday($idCard),
|
||||
'gender' => self::extractGender($idCard),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
137
Moncter/app/utils/LogMaskingProcessor.php
Normal file
137
Moncter/app/utils/LogMaskingProcessor.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace app\utils;
|
||||
|
||||
use Monolog\Processor\ProcessorInterface;
|
||||
|
||||
/**
|
||||
* 日志脱敏处理器
|
||||
*
|
||||
* 自动对日志中的敏感信息进行脱敏处理
|
||||
* 兼容 Monolog 2.x(使用 array 格式)
|
||||
*/
|
||||
class LogMaskingProcessor implements ProcessorInterface
|
||||
{
|
||||
/**
|
||||
* 敏感字段列表
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected array $sensitiveFields = [
|
||||
'id_card',
|
||||
'id_card_encrypted',
|
||||
'id_card_hash',
|
||||
'phone',
|
||||
'email',
|
||||
'password',
|
||||
'token',
|
||||
'secret',
|
||||
];
|
||||
|
||||
/**
|
||||
* 处理日志记录,对敏感信息进行脱敏
|
||||
*
|
||||
* @param array<string, mixed> $record Monolog 2.x 格式的日志记录数组
|
||||
* @return array<string, mixed> 处理后的日志记录数组
|
||||
*/
|
||||
public function __invoke(array $record): array
|
||||
{
|
||||
// 处理 context 中的敏感信息
|
||||
if (isset($record['context']) && is_array($record['context'])) {
|
||||
$record['context'] = $this->maskArray($record['context']);
|
||||
}
|
||||
|
||||
// 处理 extra 中的敏感信息
|
||||
if (isset($record['extra']) && is_array($record['extra'])) {
|
||||
$record['extra'] = $this->maskArray($record['extra']);
|
||||
}
|
||||
|
||||
// 对消息本身也进行脱敏(如果包含敏感信息)
|
||||
if (isset($record['message']) && is_string($record['message'])) {
|
||||
$record['message'] = $this->maskString($record['message']);
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏数组中的敏感字段
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function maskArray(array $data): array
|
||||
{
|
||||
$masked = [];
|
||||
foreach ($data as $key => $value) {
|
||||
$lowerKey = strtolower($key);
|
||||
|
||||
// 检查字段名是否包含敏感关键词
|
||||
$isSensitive = false;
|
||||
foreach ($this->sensitiveFields as $field) {
|
||||
if (strpos($lowerKey, $field) !== false) {
|
||||
$isSensitive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isSensitive && is_string($value)) {
|
||||
// 根据字段类型选择脱敏方法
|
||||
if (strpos($lowerKey, 'phone') !== false) {
|
||||
$masked[$key] = DataMaskingHelper::maskPhone($value);
|
||||
} elseif (strpos($lowerKey, 'email') !== false) {
|
||||
$masked[$key] = DataMaskingHelper::maskEmail($value);
|
||||
} elseif (strpos($lowerKey, 'id_card') !== false) {
|
||||
$masked[$key] = DataMaskingHelper::maskIdCard($value);
|
||||
} else {
|
||||
// 其他敏感字段,用*替代
|
||||
$masked[$key] = str_repeat('*', min(strlen($value), 20));
|
||||
}
|
||||
} elseif (is_array($value)) {
|
||||
$masked[$key] = $this->maskArray($value);
|
||||
} else {
|
||||
$masked[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $masked;
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏字符串中的敏感信息(简单模式,匹配常见格式)
|
||||
*
|
||||
* @param string $message
|
||||
* @return string
|
||||
*/
|
||||
protected function maskString(string $message): string
|
||||
{
|
||||
// 匹配身份证号(18位或15位数字)
|
||||
$message = preg_replace_callback(
|
||||
'/\b\d{15}(\d{3})?[Xx]?\b/',
|
||||
function ($matches) {
|
||||
return DataMaskingHelper::maskIdCard($matches[0]);
|
||||
},
|
||||
$message
|
||||
);
|
||||
|
||||
// 匹配手机号(11位数字,1开头)
|
||||
$message = preg_replace_callback(
|
||||
'/\b1[3-9]\d{9}\b/',
|
||||
function ($matches) {
|
||||
return DataMaskingHelper::maskPhone($matches[0]);
|
||||
},
|
||||
$message
|
||||
);
|
||||
|
||||
// 匹配邮箱
|
||||
$message = preg_replace_callback(
|
||||
'/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/',
|
||||
function ($matches) {
|
||||
return DataMaskingHelper::maskEmail($matches[0]);
|
||||
},
|
||||
$message
|
||||
);
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
|
||||
155
Moncter/app/utils/LoggerHelper.php
Normal file
155
Moncter/app/utils/LoggerHelper.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace app\utils;
|
||||
|
||||
use Monolog\Logger;
|
||||
|
||||
/**
|
||||
* 日志辅助工具类
|
||||
*
|
||||
* 提供结构化的日志记录方法
|
||||
*/
|
||||
class LoggerHelper
|
||||
{
|
||||
/**
|
||||
* 记录请求日志
|
||||
*
|
||||
* @param string $method HTTP方法
|
||||
* @param string $path 请求路径
|
||||
* @param array<string, mixed> $params 请求参数
|
||||
* @param float|null $duration 请求耗时(秒)
|
||||
*/
|
||||
public static function logRequest(string $method, string $path, array $params = [], ?float $duration = null): void
|
||||
{
|
||||
$logger = \support\Log::channel('default');
|
||||
$context = [
|
||||
'type' => 'request',
|
||||
'method' => $method,
|
||||
'path' => $path,
|
||||
'params' => $params,
|
||||
];
|
||||
|
||||
if ($duration !== null) {
|
||||
$context['duration'] = round($duration * 1000, 2) . 'ms';
|
||||
}
|
||||
|
||||
$logger->info("请求: {$method} {$path}", $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录业务日志
|
||||
*
|
||||
* @param string $action 操作名称
|
||||
* @param array<string, mixed> $context 上下文信息
|
||||
* @param string $level 日志级别(info/warning/error)
|
||||
*/
|
||||
public static function logBusiness(string $action, array $context = [], string $level = 'info'): void
|
||||
{
|
||||
$logger = \support\Log::channel('default');
|
||||
$context['type'] = 'business';
|
||||
$context['action'] = $action;
|
||||
|
||||
$logger->$level("业务操作: {$action}", $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录标签计算日志
|
||||
*
|
||||
* @param string $userId 用户ID
|
||||
* @param string $tagId 标签ID
|
||||
* @param array<string, mixed> $result 计算结果
|
||||
* @param float|null $duration 计算耗时(秒)
|
||||
*/
|
||||
public static function logTagCalculation(string $userId, string $tagId, array $result, ?float $duration = null): void
|
||||
{
|
||||
$logger = \support\Log::channel('default');
|
||||
$context = [
|
||||
'type' => 'tag_calculation',
|
||||
'user_id' => $userId,
|
||||
'tag_id' => $tagId,
|
||||
'result' => $result,
|
||||
];
|
||||
|
||||
if ($duration !== null) {
|
||||
$context['duration'] = round($duration * 1000, 2) . 'ms';
|
||||
}
|
||||
|
||||
$logger->info("标签计算: user_id={$userId}, tag_id={$tagId}", $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录错误日志
|
||||
*
|
||||
* @param \Throwable $exception 异常对象
|
||||
* @param array<string, mixed> $context 额外上下文
|
||||
*/
|
||||
public static function logError(\Throwable $exception, array $context = []): void
|
||||
{
|
||||
$logger = \support\Log::channel('default');
|
||||
$context['type'] = 'error';
|
||||
|
||||
// 限制 trace 长度,避免内存溢出
|
||||
$trace = $exception->getTraceAsString();
|
||||
$originalTraceLength = strlen($trace);
|
||||
$maxTraceLength = 5000; // 最大 trace 长度(字符数)
|
||||
|
||||
// 限制 trace 行数,只保留前50行
|
||||
$traceLines = explode("\n", $trace);
|
||||
$originalLineCount = count($traceLines);
|
||||
|
||||
if ($originalLineCount > 50) {
|
||||
$traceLines = array_slice($traceLines, 0, 50);
|
||||
$trace = implode("\n", $traceLines) . "\n... (trace truncated, total lines: {$originalLineCount})";
|
||||
}
|
||||
|
||||
// 限制 trace 字符长度
|
||||
if (strlen($trace) > $maxTraceLength) {
|
||||
$trace = substr($trace, 0, $maxTraceLength) . "\n... (trace truncated, total length: {$originalTraceLength} bytes)";
|
||||
}
|
||||
|
||||
$context['exception'] = [
|
||||
'message' => $exception->getMessage(),
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
'trace' => $trace,
|
||||
'class' => get_class($exception),
|
||||
];
|
||||
|
||||
// 如果上下文数据太大,也进行限制
|
||||
$contextJson = json_encode($context);
|
||||
if (strlen($contextJson) > 10000) {
|
||||
// 如果上下文太大,只保留关键信息
|
||||
$context = [
|
||||
'type' => 'error',
|
||||
'exception' => [
|
||||
'message' => $exception->getMessage(),
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
'class' => get_class($exception),
|
||||
'trace' => substr($trace, 0, 2000) . '... (truncated)',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$logger->error("异常: {$exception->getMessage()}", $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录性能日志
|
||||
*
|
||||
* @param string $operation 操作名称
|
||||
* @param float $duration 耗时(秒)
|
||||
* @param array<string, mixed> $context 上下文信息
|
||||
*/
|
||||
public static function logPerformance(string $operation, float $duration, array $context = []): void
|
||||
{
|
||||
$logger = \support\Log::channel('default');
|
||||
$context['type'] = 'performance';
|
||||
$context['operation'] = $operation;
|
||||
$context['duration'] = round($duration * 1000, 2) . 'ms';
|
||||
|
||||
$level = $duration > 1.0 ? 'warning' : 'info';
|
||||
$logger->$level("性能: {$operation} 耗时 {$context['duration']}", $context);
|
||||
}
|
||||
}
|
||||
|
||||
55
Moncter/app/utils/MongoDBHelper.php
Normal file
55
Moncter/app/utils/MongoDBHelper.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace app\utils;
|
||||
|
||||
use MongoDB\Client;
|
||||
|
||||
/**
|
||||
* MongoDB 连接辅助工具类
|
||||
*
|
||||
* 统一 MongoDB DSN 构建和客户端创建逻辑
|
||||
*/
|
||||
class MongoDBHelper
|
||||
{
|
||||
/**
|
||||
* 构建 MongoDB DSN 连接字符串
|
||||
*
|
||||
* @param array<string, mixed> $config 数据库配置
|
||||
* @return string DSN 字符串
|
||||
*/
|
||||
public static function buildDsn(array $config): string
|
||||
{
|
||||
$host = $config['host'] ?? '192.168.1.106';
|
||||
$port = $config['port'] ?? 27017;
|
||||
$username = $config['username'] ?? '';
|
||||
$password = $config['password'] ?? '';
|
||||
$authSource = $config['auth_source'] ?? 'admin';
|
||||
|
||||
if (!empty($username) && !empty($password)) {
|
||||
return "mongodb://{$username}:{$password}@{$host}:{$port}/{$authSource}";
|
||||
}
|
||||
|
||||
return "mongodb://{$host}:{$port}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 MongoDB 客户端
|
||||
*
|
||||
* @param array<string, mixed> $config 数据库配置
|
||||
* @param array<string, mixed> $options 额外选项(可选)
|
||||
* @return Client MongoDB 客户端实例
|
||||
*/
|
||||
public static function createClient(array $config, array $options = []): Client
|
||||
{
|
||||
$defaultOptions = [
|
||||
'connectTimeoutMS' => 5000,
|
||||
'socketTimeoutMS' => 5000,
|
||||
];
|
||||
|
||||
return new Client(
|
||||
self::buildDsn($config),
|
||||
array_merge($defaultOptions, $options)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
247
Moncter/app/utils/QueueService.php
Normal file
247
Moncter/app/utils/QueueService.php
Normal file
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
namespace app\utils;
|
||||
|
||||
use PhpAmqpLib\Connection\AMQPStreamConnection;
|
||||
use PhpAmqpLib\Message\AMQPMessage;
|
||||
use PhpAmqpLib\Channel\AMQPChannel;
|
||||
use app\utils\LoggerHelper;
|
||||
|
||||
/**
|
||||
* 队列服务封装
|
||||
*
|
||||
* 职责:
|
||||
* - 封装 RabbitMQ 连接和消息推送
|
||||
* - 提供统一的队列操作接口
|
||||
*/
|
||||
class QueueService
|
||||
{
|
||||
private static ?AMQPStreamConnection $connection = null;
|
||||
private static ?AMQPChannel $channel = null;
|
||||
private static array $config = [];
|
||||
|
||||
/**
|
||||
* 初始化连接(单例模式)
|
||||
*/
|
||||
private static function initConnection(): void
|
||||
{
|
||||
if (self::$connection !== null && self::$connection->isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$config = config('queue.connections.rabbitmq');
|
||||
self::$config = $config;
|
||||
|
||||
try {
|
||||
self::$connection = new AMQPStreamConnection(
|
||||
$config['host'],
|
||||
$config['port'],
|
||||
$config['user'],
|
||||
$config['password'],
|
||||
$config['vhost'],
|
||||
false, // insist
|
||||
'AMQPLAIN', // login_method
|
||||
null, // login_response
|
||||
'en_US', // locale
|
||||
$config['timeout'] ?? 10.0, // connection_timeout
|
||||
$config['timeout'] ?? 10.0, // read_write_timeout
|
||||
null, // context
|
||||
false, // keepalive
|
||||
$config['heartbeat'] ?? 0 // heartbeat
|
||||
);
|
||||
|
||||
self::$channel = self::$connection->channel();
|
||||
|
||||
// 声明数据同步交换机
|
||||
if (isset($config['exchanges']['data_sync'])) {
|
||||
$exchangeConfig = $config['exchanges']['data_sync'];
|
||||
self::$channel->exchange_declare(
|
||||
$exchangeConfig['name'],
|
||||
$exchangeConfig['type'],
|
||||
false, // passive
|
||||
$exchangeConfig['durable'],
|
||||
$exchangeConfig['auto_delete']
|
||||
);
|
||||
}
|
||||
|
||||
// 声明标签计算交换机
|
||||
if (isset($config['exchanges']['tag_calculation'])) {
|
||||
$exchangeConfig = $config['exchanges']['tag_calculation'];
|
||||
self::$channel->exchange_declare(
|
||||
$exchangeConfig['name'],
|
||||
$exchangeConfig['type'],
|
||||
false, // passive
|
||||
$exchangeConfig['durable'],
|
||||
$exchangeConfig['auto_delete']
|
||||
);
|
||||
}
|
||||
|
||||
// 声明队列
|
||||
if (isset($config['queues']['tag_calculation'])) {
|
||||
$queueConfig = $config['queues']['tag_calculation'];
|
||||
self::$channel->queue_declare(
|
||||
$queueConfig['name'],
|
||||
false, // passive
|
||||
$queueConfig['durable'],
|
||||
false, // exclusive
|
||||
$queueConfig['auto_delete'],
|
||||
false, // nowait
|
||||
$queueConfig['arguments'] ?? []
|
||||
);
|
||||
|
||||
// 绑定队列到交换机
|
||||
if (isset($config['routing_keys']['tag_calculation'])) {
|
||||
self::$channel->queue_bind(
|
||||
$queueConfig['name'],
|
||||
$config['exchanges']['tag_calculation']['name'],
|
||||
$config['routing_keys']['tag_calculation']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoggerHelper::logBusiness('queue_connection_established', [
|
||||
'host' => $config['host'],
|
||||
'port' => $config['port'],
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
LoggerHelper::logError($e, [
|
||||
'component' => 'QueueService',
|
||||
'action' => 'initConnection',
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送消息到数据同步队列
|
||||
*
|
||||
* @param array<string, mixed> $data 消息数据(包含数据源ID、数据记录等)
|
||||
* @return bool 是否推送成功
|
||||
*/
|
||||
public static function pushDataSync(array $data): bool
|
||||
{
|
||||
try {
|
||||
self::initConnection();
|
||||
|
||||
$config = self::$config;
|
||||
$messageConfig = config('queue.message', []);
|
||||
|
||||
$messageBody = json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
$message = new AMQPMessage(
|
||||
$messageBody,
|
||||
[
|
||||
'delivery_mode' => $messageConfig['delivery_mode'] ?? AMQPMessage::DELIVERY_MODE_PERSISTENT,
|
||||
'content_type' => $messageConfig['content_type'] ?? 'application/json',
|
||||
]
|
||||
);
|
||||
|
||||
$exchangeName = $config['exchanges']['data_sync']['name'];
|
||||
$routingKey = $config['routing_keys']['data_sync'];
|
||||
|
||||
self::$channel->basic_publish($message, $exchangeName, $routingKey);
|
||||
|
||||
LoggerHelper::logBusiness('queue_message_pushed', [
|
||||
'queue' => 'data_sync',
|
||||
'data' => $data,
|
||||
]);
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
LoggerHelper::logError($e, [
|
||||
'component' => 'QueueService',
|
||||
'action' => 'pushDataSync',
|
||||
'data' => $data,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送消息到标签计算队列
|
||||
*
|
||||
* @param array<string, mixed> $data 消息数据
|
||||
* @return bool 是否推送成功
|
||||
*/
|
||||
public static function pushTagCalculation(array $data): bool
|
||||
{
|
||||
try {
|
||||
self::initConnection();
|
||||
|
||||
$config = self::$config;
|
||||
$messageConfig = config('queue.message', []);
|
||||
|
||||
$messageBody = json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
$message = new AMQPMessage(
|
||||
$messageBody,
|
||||
[
|
||||
'delivery_mode' => $messageConfig['delivery_mode'] ?? AMQPMessage::DELIVERY_MODE_PERSISTENT,
|
||||
'content_type' => $messageConfig['content_type'] ?? 'application/json',
|
||||
]
|
||||
);
|
||||
|
||||
$exchangeName = $config['exchanges']['tag_calculation']['name'];
|
||||
$routingKey = $config['routing_keys']['tag_calculation'];
|
||||
|
||||
self::$channel->basic_publish($message, $exchangeName, $routingKey);
|
||||
|
||||
LoggerHelper::logBusiness('queue_message_pushed', [
|
||||
'queue' => 'tag_calculation',
|
||||
'data' => $data,
|
||||
]);
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
LoggerHelper::logError($e, [
|
||||
'component' => 'QueueService',
|
||||
'action' => 'pushTagCalculation',
|
||||
'data' => $data,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭连接
|
||||
*/
|
||||
public static function closeConnection(): void
|
||||
{
|
||||
try {
|
||||
if (self::$channel !== null) {
|
||||
self::$channel->close();
|
||||
self::$channel = null;
|
||||
}
|
||||
if (self::$connection !== null && self::$connection->isConnected()) {
|
||||
self::$connection->close();
|
||||
self::$connection = null;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
LoggerHelper::logError($e, [
|
||||
'component' => 'QueueService',
|
||||
'action' => 'closeConnection',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通道(用于消费者)
|
||||
*
|
||||
* @return AMQPChannel
|
||||
*/
|
||||
public static function getChannel(): AMQPChannel
|
||||
{
|
||||
self::initConnection();
|
||||
return self::$channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接(用于消费者)
|
||||
*
|
||||
* @return AMQPStreamConnection
|
||||
*/
|
||||
public static function getConnection(): AMQPStreamConnection
|
||||
{
|
||||
self::initConnection();
|
||||
return self::$connection;
|
||||
}
|
||||
}
|
||||
|
||||
267
Moncter/app/utils/RedisHelper.php
Normal file
267
Moncter/app/utils/RedisHelper.php
Normal file
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
|
||||
namespace app\utils;
|
||||
|
||||
use Predis\Client;
|
||||
use app\utils\LoggerHelper;
|
||||
|
||||
/**
|
||||
* Redis 工具类
|
||||
*
|
||||
* 职责:
|
||||
* - 封装 Redis 连接和基础操作
|
||||
* - 提供分布式锁功能
|
||||
*/
|
||||
class RedisHelper
|
||||
{
|
||||
private static ?Client $client = null;
|
||||
private static array $config = [];
|
||||
|
||||
/**
|
||||
* 获取 Redis 客户端(单例模式)
|
||||
*
|
||||
* @return Client Redis 客户端
|
||||
*/
|
||||
public static function getClient(): Client
|
||||
{
|
||||
if (self::$client !== null) {
|
||||
return self::$client;
|
||||
}
|
||||
|
||||
// 从 session 配置中读取 Redis 配置(临时方案,后续可创建独立的 cache.php)
|
||||
$sessionConfig = config('session.config.redis', []);
|
||||
|
||||
self::$config = [
|
||||
'host' => $sessionConfig['host'] ?? getenv('REDIS_HOST') ?: '127.0.0.1',
|
||||
'port' => (int)($sessionConfig['port'] ?? getenv('REDIS_PORT') ?: 6379),
|
||||
'password' => $sessionConfig['auth'] ?? getenv('REDIS_PASSWORD') ?: null,
|
||||
'database' => (int)($sessionConfig['database'] ?? getenv('REDIS_DATABASE') ?: 0),
|
||||
'timeout' => $sessionConfig['timeout'] ?? 2.0,
|
||||
];
|
||||
|
||||
$parameters = [
|
||||
'host' => self::$config['host'],
|
||||
'port' => self::$config['port'],
|
||||
];
|
||||
|
||||
if (!empty(self::$config['password'])) {
|
||||
$parameters['password'] = self::$config['password'];
|
||||
}
|
||||
|
||||
if (self::$config['database'] > 0) {
|
||||
$parameters['database'] = self::$config['database'];
|
||||
}
|
||||
|
||||
$options = [
|
||||
'timeout' => self::$config['timeout'],
|
||||
];
|
||||
|
||||
self::$client = new Client($parameters, $options);
|
||||
|
||||
// 测试连接
|
||||
try {
|
||||
self::$client->ping();
|
||||
LoggerHelper::logBusiness('redis_connected', [
|
||||
'host' => self::$config['host'],
|
||||
'port' => self::$config['port'],
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
LoggerHelper::logError($e, [
|
||||
'component' => 'RedisHelper',
|
||||
'action' => 'getClient',
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return self::$client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分布式锁
|
||||
*
|
||||
* @param string $key 锁的键
|
||||
* @param int $ttl 锁的过期时间(秒)
|
||||
* @param int $retryTimes 重试次数
|
||||
* @param int $retryDelay 重试延迟(毫秒)
|
||||
* @return bool 是否获取成功
|
||||
*/
|
||||
public static function acquireLock(string $key, int $ttl = 300, int $retryTimes = 3, int $retryDelay = 1000): bool
|
||||
{
|
||||
$client = self::getClient();
|
||||
$lockKey = "lock:{$key}";
|
||||
$lockValue = uniqid(gethostname() . '_', true); // 唯一值,用于安全释放锁
|
||||
|
||||
for ($i = 0; $i <= $retryTimes; $i++) {
|
||||
// 尝试获取锁(SET key value NX EX ttl)
|
||||
$result = $client->set($lockKey, $lockValue, 'EX', $ttl, 'NX');
|
||||
|
||||
if ($result) {
|
||||
LoggerHelper::logBusiness('redis_lock_acquired', [
|
||||
'key' => $key,
|
||||
'ttl' => $ttl,
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果还有重试机会,等待后重试
|
||||
if ($i < $retryTimes) {
|
||||
usleep($retryDelay * 1000); // 转换为微秒
|
||||
}
|
||||
}
|
||||
|
||||
LoggerHelper::logBusiness('redis_lock_failed', [
|
||||
'key' => $key,
|
||||
'retry_times' => $retryTimes,
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放分布式锁
|
||||
*
|
||||
* @param string $key 锁的键
|
||||
* @return bool 是否释放成功
|
||||
*/
|
||||
public static function releaseLock(string $key): bool
|
||||
{
|
||||
$client = self::getClient();
|
||||
$lockKey = "lock:{$key}";
|
||||
|
||||
try {
|
||||
$result = $client->del([$lockKey]);
|
||||
|
||||
if ($result > 0) {
|
||||
LoggerHelper::logBusiness('redis_lock_released', [
|
||||
'key' => $key,
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
LoggerHelper::logError($e, [
|
||||
'component' => 'RedisHelper',
|
||||
'action' => 'releaseLock',
|
||||
'key' => $key,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键值对
|
||||
*
|
||||
* @param string $key 键
|
||||
* @param mixed $value 值
|
||||
* @param int|null $ttl 过期时间(秒),null 表示不过期
|
||||
* @return bool 是否设置成功
|
||||
*/
|
||||
public static function set(string $key, $value, ?int $ttl = null): bool
|
||||
{
|
||||
try {
|
||||
$client = self::getClient();
|
||||
$serialized = is_string($value) ? $value : json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
if ($ttl !== null) {
|
||||
$client->setex($key, $ttl, $serialized);
|
||||
} else {
|
||||
$client->set($key, $serialized);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
LoggerHelper::logError($e, [
|
||||
'component' => 'RedisHelper',
|
||||
'action' => 'set',
|
||||
'key' => $key,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键值
|
||||
*
|
||||
* @param string $key 键
|
||||
* @return mixed 值,不存在返回 null
|
||||
*/
|
||||
public static function get(string $key)
|
||||
{
|
||||
try {
|
||||
$client = self::getClient();
|
||||
$value = $client->get($key);
|
||||
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 尝试 JSON 解码
|
||||
$decoded = json_decode($value, true);
|
||||
return $decoded !== null ? $decoded : $value;
|
||||
} catch (\Throwable $e) {
|
||||
LoggerHelper::logError($e, [
|
||||
'component' => 'RedisHelper',
|
||||
'action' => 'get',
|
||||
'key' => $key,
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除键
|
||||
*
|
||||
* @param string $key 键
|
||||
* @return bool 是否删除成功
|
||||
*/
|
||||
public static function delete(string $key): bool
|
||||
{
|
||||
try {
|
||||
$client = self::getClient();
|
||||
$result = $client->del([$key]);
|
||||
return $result > 0;
|
||||
} catch (\Throwable $e) {
|
||||
LoggerHelper::logError($e, [
|
||||
'component' => 'RedisHelper',
|
||||
'action' => 'delete',
|
||||
'key' => $key,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查键是否存在
|
||||
*
|
||||
* @param string $key 键
|
||||
* @return bool 是否存在
|
||||
*/
|
||||
public static function exists(string $key): bool
|
||||
{
|
||||
try {
|
||||
$client = self::getClient();
|
||||
$result = $client->exists($key);
|
||||
return $result > 0;
|
||||
} catch (\Throwable $e) {
|
||||
LoggerHelper::logError($e, [
|
||||
'component' => 'RedisHelper',
|
||||
'action' => 'exists',
|
||||
'key' => $key,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除键(别名,兼容del方法)
|
||||
*
|
||||
* @param string $key 键
|
||||
* @return bool 是否删除成功
|
||||
*/
|
||||
public static function del(string $key): bool
|
||||
{
|
||||
return self::delete($key);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user