实现后台及前台通过API访问UV埋点,所有代码全部保存
This commit is contained in:
346
src/app/common/library/BrowserDetector.php
Normal file
346
src/app/common/library/BrowserDetector.php
Normal file
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
// app/common/library/BrowserDetector.php
|
||||
namespace app\common\library;
|
||||
|
||||
class BrowserDetector
|
||||
{
|
||||
private $userAgent;
|
||||
private $headers;
|
||||
private $server;
|
||||
private $request;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
$this->headers = $this->getAllHeaders();
|
||||
$this->server = $_SERVER;
|
||||
$this->request = $_REQUEST;
|
||||
}
|
||||
|
||||
/**
|
||||
* 综合检测方法
|
||||
*/
|
||||
public function detect()
|
||||
{
|
||||
return [
|
||||
// 基础信息
|
||||
'user_agent' => $this->userAgent,
|
||||
'ip_address' => $this->getClientIp(),
|
||||
'request_time' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']),
|
||||
|
||||
// 平台类型
|
||||
'is_wechat' => $this->isWeChat(),
|
||||
'is_alipay' => $this->isAlipay(),
|
||||
'is_dingtalk' => $this->isDingTalk(),
|
||||
'is_miniprogram' => $this->isMiniProgram(),
|
||||
|
||||
// 设备类型
|
||||
'is_mobile' => $this->isMobile(),
|
||||
'is_tablet' => $this->isTablet(),
|
||||
'is_desktop' => $this->isDesktop(),
|
||||
|
||||
// 浏览器类型
|
||||
'is_chrome' => $this->isChrome(),
|
||||
'is_firefox' => $this->isFirefox(),
|
||||
'is_safari' => $this->isSafari(),
|
||||
'is_edge' => $this->isEdge(),
|
||||
'is_ie' => $this->isIE(),
|
||||
|
||||
// 操作系统
|
||||
'is_windows' => $this->isWindows(),
|
||||
'is_mac' => $this->isMac(),
|
||||
'is_linux' => $this->isLinux(),
|
||||
'is_ios' => $this->isIOS(),
|
||||
'is_android' => $this->isAndroid(),
|
||||
|
||||
// 来源类型
|
||||
'source_type' => $this->getSourceType(),
|
||||
'source_name' => $this->getSourceName(),
|
||||
|
||||
// URL参数
|
||||
'referer' => $_SERVER['HTTP_REFERER'] ?? '',
|
||||
'utm_source' => $_GET['utm_source'] ?? '',
|
||||
'utm_medium' => $_GET['utm_medium'] ?? '',
|
||||
'utm_campaign' => $_GET['utm_campaign'] ?? '',
|
||||
|
||||
// 自定义来源
|
||||
'custom_source' => $this->getCustomSource(),
|
||||
|
||||
// 请求信息
|
||||
'request_method' => $_SERVER['REQUEST_METHOD'],
|
||||
'request_uri' => $_SERVER['REQUEST_URI'],
|
||||
'query_string' => $_SERVER['QUERY_STRING'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有请求头
|
||||
*/
|
||||
private function getAllHeaders()
|
||||
{
|
||||
if (function_exists('getallheaders')) {
|
||||
return getallheaders();
|
||||
}
|
||||
|
||||
$headers = [];
|
||||
foreach ($_SERVER as $name => $value) {
|
||||
if (substr($name, 0, 5) == 'HTTP_') {
|
||||
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP
|
||||
*/
|
||||
private function getClientIp()
|
||||
{
|
||||
$ip = '';
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
return $ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断微信环境
|
||||
*/
|
||||
public function isWeChat()
|
||||
{
|
||||
return stripos($this->userAgent, 'micromessenger') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断支付宝环境
|
||||
*/
|
||||
public function isAlipay()
|
||||
{
|
||||
return stripos($this->userAgent, 'alipay') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断钉钉环境
|
||||
*/
|
||||
public function isDingTalk()
|
||||
{
|
||||
return stripos($this->userAgent, 'dingtalk') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断小程序环境
|
||||
*/
|
||||
public function isMiniProgram()
|
||||
{
|
||||
// 微信小程序
|
||||
if ($this->isWeChat() && stripos($this->userAgent, 'miniprogram') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 支付宝小程序
|
||||
if ($this->isAlipay() && stripos($this->userAgent, 'miniprogram') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 通过Referer判断(小程序跳转H5时)
|
||||
$referer = $_SERVER['HTTP_REFERER'] ?? '';
|
||||
if (strpos($referer, 'servicewechat.com') !== false ||
|
||||
strpos($referer, 'alipay.com') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断移动设备
|
||||
*/
|
||||
public function isMobile()
|
||||
{
|
||||
return preg_match('/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i', $this->userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断平板设备
|
||||
*/
|
||||
public function isTablet()
|
||||
{
|
||||
return preg_match('/ipad|android(?!.*mobile)|tablet/i', $this->userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断桌面设备
|
||||
*/
|
||||
public function isDesktop()
|
||||
{
|
||||
return !$this->isMobile() && !$this->isTablet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器类型判断
|
||||
*/
|
||||
public function isChrome()
|
||||
{
|
||||
return stripos($this->userAgent, 'chrome') !== false && stripos($this->userAgent, 'edge') === false;
|
||||
}
|
||||
|
||||
public function isFirefox()
|
||||
{
|
||||
return stripos($this->userAgent, 'firefox') !== false;
|
||||
}
|
||||
|
||||
public function isSafari()
|
||||
{
|
||||
return stripos($this->userAgent, 'safari') !== false && stripos($this->userAgent, 'chrome') === false;
|
||||
}
|
||||
|
||||
public function isEdge()
|
||||
{
|
||||
return stripos($this->userAgent, 'edge') !== false;
|
||||
}
|
||||
|
||||
public function isIE()
|
||||
{
|
||||
return stripos($this->userAgent, 'msie') !== false || stripos($this->userAgent, 'trident') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作系统判断
|
||||
*/
|
||||
public function isWindows()
|
||||
{
|
||||
return stripos($this->userAgent, 'windows') !== false;
|
||||
}
|
||||
|
||||
public function isMac()
|
||||
{
|
||||
return stripos($this->userAgent, 'macintosh') !== false || stripos($this->userAgent, 'mac os x') !== false;
|
||||
}
|
||||
|
||||
public function isLinux()
|
||||
{
|
||||
return stripos($this->userAgent, 'linux') !== false && !$this->isAndroid();
|
||||
}
|
||||
|
||||
public function isIOS()
|
||||
{
|
||||
return preg_match('/iphone|ipad|ipod/i', $this->userAgent);
|
||||
}
|
||||
|
||||
public function isAndroid()
|
||||
{
|
||||
return stripos($this->userAgent, 'android') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自定义来源
|
||||
*/
|
||||
private function getCustomSource()
|
||||
{
|
||||
return [
|
||||
'from' => $_GET['from'] ?? '',
|
||||
'source' => $_GET['source'] ?? '',
|
||||
'channel' => $_GET['channel'] ?? '',
|
||||
'campaign' => $_GET['campaign'] ?? ''
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 综合判断来源类型
|
||||
*/
|
||||
public function getSourceType()
|
||||
{
|
||||
if ($this->isMiniProgram()) {
|
||||
return 'miniprogram';
|
||||
} elseif ($this->isWeChat()) {
|
||||
return 'wechat';
|
||||
} elseif ($this->isAlipay()) {
|
||||
return 'alipay';
|
||||
} elseif ($this->isMobile()) {
|
||||
return 'h5';
|
||||
} elseif ($this->isDesktop()) {
|
||||
return 'pc';
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详细的来源名称
|
||||
*/
|
||||
public function getSourceName()
|
||||
{
|
||||
$sourceType = $this->getSourceType();
|
||||
|
||||
switch ($sourceType) {
|
||||
case 'miniprogram':
|
||||
if ($this->isWeChat()) return 'wechat_miniprogram';
|
||||
if ($this->isAlipay()) return 'alipay_miniprogram';
|
||||
return 'miniprogram';
|
||||
|
||||
case 'wechat':
|
||||
if ($this->isIOS()) return 'wechat_ios';
|
||||
if ($this->isAndroid()) return 'wechat_android';
|
||||
return 'wechat';
|
||||
|
||||
case 'h5':
|
||||
if ($this->isChrome()) return 'h5_chrome';
|
||||
if ($this->isSafari()) return 'h5_safari';
|
||||
return 'h5';
|
||||
|
||||
case 'pc':
|
||||
if ($this->isChrome()) return 'pc_chrome';
|
||||
if ($this->isFirefox()) return 'pc_firefox';
|
||||
if ($this->isEdge()) return 'pc_edge';
|
||||
return 'pc';
|
||||
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成客户端指纹
|
||||
*/
|
||||
public function generateFingerprint()
|
||||
{
|
||||
$components = [
|
||||
$this->userAgent,
|
||||
$_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '',
|
||||
$this->getClientIp(),
|
||||
$_SERVER['HTTP_ACCEPT'] ?? '',
|
||||
$_SERVER['HTTP_ACCEPT_ENCODING'] ?? ''
|
||||
];
|
||||
|
||||
return md5(implode('|', $components));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存检测结果到日志
|
||||
*/
|
||||
public function saveToLog($filename = 'browser_detection.log')
|
||||
{
|
||||
$data = $this->detect();
|
||||
$logEntry = json_encode($data, JSON_UNESCAPED_UNICODE) . PHP_EOL;
|
||||
file_put_contents($filename, $logEntry, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
public function getStats()
|
||||
{
|
||||
return [
|
||||
'user_agent_length' => strlen($this->userAgent),
|
||||
'header_count' => count($this->headers),
|
||||
'is_secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
|
||||
'is_ajax' => !empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest',
|
||||
'is_post' => $_SERVER['REQUEST_METHOD'] === 'POST'
|
||||
];
|
||||
}
|
||||
}
|
||||
112
src/app/common/library/CallerInfo.php
Normal file
112
src/app/common/library/CallerInfo.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
namespace app\common\library;
|
||||
/**
|
||||
* 获取调用者信息的基础类
|
||||
*/
|
||||
class CallerInfo
|
||||
{
|
||||
/**
|
||||
* 获取调用者信息
|
||||
* @param int $depth 调用深度(0-当前函数,1-直接调用者,2-调用者的调用者)
|
||||
* @return array
|
||||
*/
|
||||
public static function getCallerInfo(int $depth = 1): array
|
||||
{
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $depth + 2);
|
||||
|
||||
if (!isset($backtrace[$depth])) {
|
||||
return [
|
||||
'file' => 'unknown',
|
||||
'line' => 0,
|
||||
'function' => 'unknown',
|
||||
'class' => 'unknown',
|
||||
'type' => 'unknown'
|
||||
];
|
||||
}
|
||||
|
||||
$caller = $backtrace[$depth];
|
||||
|
||||
return [
|
||||
'file' => $caller['file'] ?? 'unknown',
|
||||
'line' => $caller['line'] ?? 0,
|
||||
'function' => $caller['function'] ?? 'unknown',
|
||||
'class' => $caller['class'] ?? 'unknown',
|
||||
'type' => $caller['type'] ?? 'unknown', // '->' 或 '::'
|
||||
'args_count' => isset($caller['args']) ? count($caller['args']) : 0
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调用者文件路径
|
||||
*/
|
||||
public static function getCallerFile(int $depth = 1): string
|
||||
{
|
||||
$info = self::getCallerInfo($depth + 1);
|
||||
return $info['file'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调用者行号
|
||||
*/
|
||||
public static function getCallerLine(int $depth = 1): int
|
||||
{
|
||||
$info = self::getCallerInfo($depth + 1);
|
||||
return $info['line'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调用栈信息(完整)
|
||||
*/
|
||||
public static function getCallStack(int $limit = 10): array
|
||||
{
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $limit);
|
||||
$stack = [];
|
||||
|
||||
// 跳过当前函数
|
||||
array_shift($backtrace);
|
||||
|
||||
foreach ($backtrace as $index => $trace) {
|
||||
$stack[] = [
|
||||
'level' => $index,
|
||||
'file' => $trace['file'] ?? 'internal',
|
||||
'line' => $trace['line'] ?? 0,
|
||||
'function' => $trace['function'] ?? 'unknown',
|
||||
'class' => $trace['class'] ?? '',
|
||||
'type' => $trace['type'] ?? '',
|
||||
'args' => isset($trace['args']) ? count($trace['args']) : 0
|
||||
];
|
||||
}
|
||||
|
||||
return $stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调用链字符串(用于日志)
|
||||
*/
|
||||
public static function getCallChain(): string
|
||||
{
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5);
|
||||
$chain = [];
|
||||
|
||||
// 跳过当前函数
|
||||
array_shift($backtrace);
|
||||
|
||||
foreach ($backtrace as $trace) {
|
||||
$call = '';
|
||||
|
||||
if (isset($trace['class'])) {
|
||||
$call .= $trace['class'] . $trace['type'];
|
||||
}
|
||||
|
||||
$call .= $trace['function'];
|
||||
|
||||
if (isset($trace['file'])) {
|
||||
$call .= ' (' . basename($trace['file']) . ':' . $trace['line'] . ')';
|
||||
}
|
||||
|
||||
$chain[] = $call;
|
||||
}
|
||||
|
||||
return implode(' -> ', array_reverse($chain));
|
||||
}
|
||||
}
|
||||
115
src/app/common/library/EnhancedLogger.php
Normal file
115
src/app/common/library/EnhancedLogger.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
namespace app\common\library;
|
||||
|
||||
use app\common\library\CallerInfo;
|
||||
|
||||
/**
|
||||
* 增强的日志类(包含调用者信息)
|
||||
*/
|
||||
class EnhancedLogger
|
||||
{
|
||||
/**
|
||||
* 记录日志(包含调用者信息)
|
||||
*/
|
||||
public static function log(string $message, string $level = 'info', array $context = []): void
|
||||
{
|
||||
$callerInfo = CallerInfo::getCallerInfo(2); // 跳过log方法本身
|
||||
|
||||
$logData = [
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'level' => strtoupper($level),
|
||||
'message' => $message,
|
||||
'context' => $context,
|
||||
'caller' => [
|
||||
'file' => $callerInfo['file'],
|
||||
'line' => $callerInfo['line'],
|
||||
'function' => $callerInfo['function'],
|
||||
'class' => $callerInfo['class']
|
||||
],
|
||||
'memory_usage' => memory_get_usage(true),
|
||||
'peak_memory' => memory_get_peak_usage(true)
|
||||
];
|
||||
|
||||
// 写入日志文件
|
||||
self::writeToFile($logData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试日志(自动包含调用栈)
|
||||
*/
|
||||
public static function debug(string $message, array $context = []): void
|
||||
{
|
||||
$callStack = CallerInfo::getCallStack(5);
|
||||
|
||||
self::log($message, 'debug', array_merge($context, [
|
||||
'call_stack' => $callStack
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能日志
|
||||
*/
|
||||
public static function profile(string $operation, callable $callback, array $context = []): mixed
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$startMemory = memory_get_usage(true);
|
||||
|
||||
try {
|
||||
$result = $callback();
|
||||
|
||||
$endTime = microtime(true);
|
||||
$endMemory = memory_get_usage(true);
|
||||
|
||||
self::log($operation, 'profile', array_merge($context, [
|
||||
'execution_time' => round(($endTime - $startTime) * 1000, 2) . 'ms',
|
||||
'memory_used' => self::formatBytes($endMemory - $startMemory),
|
||||
'success' => true
|
||||
]));
|
||||
|
||||
return $result;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$endTime = microtime(true);
|
||||
|
||||
self::log($operation, 'profile', array_merge($context, [
|
||||
'execution_time' => round(($endTime - $startTime) * 1000, 2) . 'ms',
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]));
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private static function writeToFile(array $logData): void
|
||||
{
|
||||
$logLine = sprintf(
|
||||
"[%s] %s: %s [%s:%d in %s::%s()] %s\n",
|
||||
$logData['timestamp'],
|
||||
$logData['level'],
|
||||
$logData['message'],
|
||||
basename($logData['caller']['file']),
|
||||
$logData['caller']['line'],
|
||||
$logData['caller']['class'],
|
||||
$logData['caller']['function'],
|
||||
json_encode($logData['context'], JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
|
||||
file_put_contents(
|
||||
__DIR__ . '/../logs/app.log',
|
||||
$logLine,
|
||||
FILE_APPEND | LOCK_EX
|
||||
);
|
||||
}
|
||||
|
||||
private static function formatBytes(int $bytes): string
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
$bytes /= pow(1024, $pow);
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$pow];
|
||||
}
|
||||
}
|
||||
63
src/app/common/library/PerformanceAwareCallerInfo.php
Normal file
63
src/app/common/library/PerformanceAwareCallerInfo.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
namespace app\common\library;
|
||||
|
||||
use think\facade\App;
|
||||
|
||||
class PerformanceAwareCallerInfo
|
||||
{
|
||||
private static $enabled = true;
|
||||
|
||||
/**
|
||||
* 启用/禁用调用者信息(生产环境可禁用)
|
||||
*/
|
||||
public static function setEnabled(bool $enabled): void
|
||||
{
|
||||
self::$enabled = $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否启用
|
||||
*/
|
||||
public static function isEnabled(): bool
|
||||
{
|
||||
return self::$enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 高性能的调用者信息获取
|
||||
*/
|
||||
public static function getCallerInfoOpt(int $depth = 1): array
|
||||
{
|
||||
if (!self::$enabled) {
|
||||
return ['file' => 'disabled', 'line' => 0];
|
||||
}
|
||||
|
||||
// 生产环境使用更轻量的方式
|
||||
if (app()->isDebug()) {
|
||||
// 开发环境:详细信息
|
||||
return self::getDetailedCallerInfo($depth);
|
||||
} else {
|
||||
// 生产环境:基本信息
|
||||
return self::getBasicCallerInfo($depth);
|
||||
}
|
||||
}
|
||||
|
||||
private static function getDetailedCallerInfo(int $depth): array
|
||||
{
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $depth + 2);
|
||||
return $backtrace[$depth] ?? ['file' => 'unknown', 'line' => 0];
|
||||
}
|
||||
|
||||
private static function getBasicCallerInfo(int $depth): array
|
||||
{
|
||||
// 使用更轻量的方法
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $depth + 2);
|
||||
$caller = $backtrace[$depth] ?? [];
|
||||
|
||||
return [
|
||||
'file' => $caller['file'] ?? 'unknown',
|
||||
'line' => $caller['line'] ?? 0,
|
||||
'function' => $caller['function'] ?? 'unknown'
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user