实现后台及前台通过API访问UV埋点,所有代码全部保存

This commit is contained in:
2025-11-08 18:15:26 +08:00
parent 6bad32d9b1
commit e440631275
43 changed files with 5960 additions and 1105 deletions

View 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'
];
}
}

View 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));
}
}

View 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];
}
}

View 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'
];
}
}