我将提供完整的代码实现,使用注解中间件方式收集特定方法的操作日志并存入 t_operation_log 表。
CREATE TABLE `operation_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`uid` bigint(20) DEFAULT NULL COMMENT '会员id',
`nickname` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户名',
`method` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '请求方式',
`api` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '请求路由',
`router` varchar(500) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '后端路由',
`title` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '业务名称',
`ip` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '请求IP地址',
`ip_country` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '请求IP地址所属国家',
`ip_province` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '请求ip所属省',
`device_id` bigint(20) DEFAULT NULL COMMENT '请求设备唯一标识',
`device_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '请求设备名称',
`browser_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '请求浏览器名称',
`browser_version` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '请求浏览器版本',
`user_agent` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '请求头',
`client_type` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '设备类型',
`request_params` json DEFAULT NULL COMMENT '请求入参',
`response_params` json DEFAULT NULL COMMENT '出参',
`remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`create_time` bigint(20) DEFAULT NULL COMMENT '创建时间',
`update_time` bigint(20) DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `user_operation_log_username_index` (`nickname`) USING BTREE,
KEY `index_memberid` (`member_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=88 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表';
1. 创建自定义注解类
<?php
declare(strict_types=1);
namespace appannotation;
use Attribute;
class Loggable
{
public function __construct(
public string $title = '', // 业务名称
public bool $logParams = true, // 是否记录请求参数
public bool $logResult = false, // 是否记录响应结果
public bool $recordIp = true, // 是否记录IP信息
public bool $recordDevice = true, // 是否记录设备信息
) {}
}
2. 创建操作日志模型
<?php
namespace appmodel;
use thinkModel;
class OperationLog extends Model
{
protected $table = 'operation_log';
protected $pk = 'id';
protected $autoWriteTimestamp = true;
protected $createTime = 'create_time';
protected $updateTime = 'update_time';
protected $json = ['request_params', 'response_params'];
protected $jsonAssoc = true;
public static function onBeforeWrite($model)
{
if (is_string($model->user_agent) {
$model->user_agent = mb_substr($model->user_agent, 0, 1024);
}
if (is_string($model->router)) {
$model->router = mb_substr($model->router, 0, 500);
}
if (is_string($model->title)) {
$model->title = mb_substr($model->title, 0, 256);
}
}
}
3. 创建日志中间件
<?php
declare(strict_types=1);
namespace appadminmiddleware;
use appadminmodelLogsOperationLog;
use appadminmodelMember;
use appannotationLogOperation;
use ReflectionMethod;
use thinkfacadeLog;
use thinkRequest;
use thinkResponse;
class LogOperationMiddleware
{
private static $annotationCache = [];
public function handle($request, Closure $next)
{
$response = $next($request);
$controller = $request->controller();
$action = $request->action();
$class = "app\admin\controller\{$controller}";
$class = str_replace('.', '\', $class);
$cacheKey = "{$class}\{$action}";
if (array_key_exists($cacheKey, self::$annotationCache)) {
$logOperation = self::$annotationCache[$cacheKey];
} else {
$logOperation = $this->resolveLogOperationAnnotation($class, $action);
self::$annotationCache[$cacheKey] = $logOperation;
}
if (!$logOperation) {
return $response;
}
try {
$memberId = tinywanJWT::getCurrentId();
} catch (Exception $e) {
$memberId = 1;
}
$startTime = microtime(true);
$executeTime = round((microtime(true) - $startTime) * 1000, 2);
$this->saveToDatabase($logOperation, $request, $response, $controller, $action, $executeTime ,$memberId,$cacheKey);
return $response;
}
protected function resolveLogOperationAnnotation(string $class, string $action): ?LogOperation
{
try {
if (!class_exists($class)) {
return null;
}
$reflect = new ReflectionMethod($class, $action);
$attributes = $reflect->getAttributes(LogOperation::class);
if (empty($attributes)) {
return null;
}
return $attributes[0]->newInstance();
} catch (ReflectionException $e) {
Log::error("日志中间件反射异常: {$class}::{$action} - " . $e->getMessage());
return null;
}
}
protected function saveToDatabase(
LogOperation $logOperation,
Request $request,
$response,
string $controller,
string $action,
float $executeTime,
int $memberId,
string $cacheKey
) {
try {
$nickname = Member::query()->where('id', $memberId)->value('nickname');
$logData = [
'member_id' => $memberId,
'nickname' => $nickname,
'method' => $request->method(),
'api' => '/admin/'.$controller . '/' . $action,
'router' => $cacheKey,
'title' => $logOperation->title ?: ($controller . '@' . $action),
'remark' => "执行耗时: {$executeTime}ms",
];
if ($logOperation->logParams) {
$params = $request->all();
$params = $this->filterSensitiveFields($params);
$logData['request_params'] = $params;
}
if ($logOperation->logResult && $response instanceof Response) {
$content = $response->getContent();
$logData['response_params'] = json_decode($content, true) ?: $content;
if (is_string($logData['response_params']) && mb_strlen($logData['response_params']) > 2000) {
$logData['response_params'] = mb_substr($logData['response_params'], 0, 2000) . '... [TRUNCATED]';
}
}
if ($logOperation->recordIp || $logOperation->recordDevice) {
$this->addDeviceInfo($logOperation, $request, $logData);
}
$res = OperationLog::create($logData);
} catch (Throwable $e) {
Log::error('操作日志记录失败: ' . $e->getMessage());
}
}
protected function addDeviceInfo(LogOperation $logOperation, Request $request, array &$logData)
{
$userAgent = $request->header('user-agent', '');
$logData['user_agent'] = $userAgent;
$ip = getClientIp($request);
$logData['ip'] = $ip ? : 'unknown';
$os = getOSInfo($userAgent);
$logData['client_type'] = $os['type'] ? : 'unknown';
$logData['device_name'] = $os['os'] ? : 'unknown';
$logData['browser_name'] = $os['browser_name'] ? : 'unknown';
$logData['browser_version'] = $os['version'] ? : 'unknown';
if($ip=='127.0.0.1' || $ip=='::1'){
$logData['ip_country'] = '--';
$logData['ip_province'] = '内网IP';
}else{
$country = getCountryCode($ip);
if($country && is_array($country)){
$logData['ip_country'] = $country['country'] ? : 'unknown';
$logData['ip_province'] = $country['city'] ? : 'unknown';
}
}
}
protected function filterSensitiveFields(array $params): array
{
$sensitiveFields = ['password', 'pay_password', 'token', 'card_number', 'cvv', 'expiry_date'];
foreach ($sensitiveFields as $field) {
if (isset($params[$field])) {
$params[$field] = '******';
}
}
return $params;
}
}
4. 注册中间件
```null <?php
return [ appadminmiddlewareLogOperationMiddleware::class, ];
### 5.定义公共方法获取ip等信息
```null
<?php
use appadminmodelConfigConfigServer;
use appadminmodelConfigConfig AS ConfigModel;
use GuzzleHttpClient;
use thinkhelperArr;
use thinkRequest;
if(! function_exists('getClientIp')){
function getClientIp(Request $request): string
{
$xForwardedFor = $request->header('x-forwarded-for', '');
if ($xForwardedFor) {
$ips = explode(',', $xForwardedFor);
$clientIp = trim($ips[0]);
if (isValidIp($clientIp)) {
return $clientIp;
}
}
$xRealIp = $request->header('x-real-ip', '');
if ($xRealIp && isValidIp($xRealIp)) {
return $xRealIp;
}
return $request->ip() ?? 'unknown';
}
}
if(! function_exists('isValidIp')) {
function isValidIp(string $ip): bool
{
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) !== false;
}
}
if (! function_exists('getCountryCode')) {
function getCountryCode($ip)
{
if(!$ip){
return '';
}
$client = new Client();
$response = $client->get("http://ip-api.com/json/{$ip}?fields=country,city");
$data = json_decode((string)$response->getBody(), true);
return $data ?? '';
}
}
if (!function_exists('getOSInfo')) {
function getOSInfo($agent): array
{
if (!isset($agent)) {
return array();
}
$type = 'pc';
$version = '';
if (preg_match('/AppleWebKit.*Mobile|Android/i', $agent) || preg_match('/MIDP|SymbianOS|NOKIA|SAMSUNG|LG|NEC|TCL|Alcatel|BIRD|DBTEL|Dopod|PHILIPS|HAIER|LENOVO|MOT-|Nokia|SonyEricsson|SIE-|Amoi|ZTE/i', $agent)) {
$type = 'mobile';
if (strpos($agent, 'iPhone') !== false) {
$agent_os = 'iPhone OS';
preg_match("/(?<=CPU iPhone OS )[d_]{1,}/", $agent, $match);
if ($match && isset($match[0])) {
$version = str_replace('_', '.', $match[0]);
}
} else if (strpos($agent, 'iPad') !== false) {
$agent_os = 'iPad OS';
preg_match("/(?<=CPU OS )[d_]{1,}/", $agent, $match);
if ($match && isset($match[0])) {
$version = str_replace('_', '.', $match[0]);
}
} else if (strpos($agent, 'Android') !== false) {
preg_match("/(?<=Android )[d.]{1,}/", $agent, $match);
$agent_os = 'Android';
if ($match && isset($match[0])) {
$version = $match[0];
}
} else if (strpos($agent, 'Windows Phone')) {
$agent_os = 'Windows Phone';
$version = 10;
} else if (stripos($agent, 'symbian') !== false) {
$agent_os = 'SymbianOS';
} else {
$agent_os = 'other';
}
} else if (strpos($agent, 'Windows') !== false) {
$os_win = array(
'NT 10.0' => 'Windows 10',
'NT 6.4' => 'Windows 10',
'NT 6.3' => 'Windows 8',
'NT 6.2' => 'Windows 8',
'NT 6.1' => 'Windows 7',
'NT 6.0' => 'Windows Vista',
'NT 5.1' => 'Windows XP',
'NT 5.0' => 'Windows 2000',
'NT' => 'Windows NT',
);
$agent_os = 'Windows';
foreach ($os_win as $core => $os) {
if (stripos($agent, $core) !== false) {
$agent_os = $os;
break;
}
}
} else if (stripos($agent, 'mac') !== false) {
$agent_os = 'Mac OS';
} else if (stripos($agent, 'ubuntu') !== false) {
$agent_os = 'Ubuntu';
} else if (stripos($agent, 'debian') !== false) {
$agent_os = 'Debian';
} else if (stripos($agent, 'linux') !== false) {
$agent_os = 'Linux';
} else {
$agent_os = 'other';
}
return ['type' => $type, 'os' => $agent_os, 'version' => $version,'browser_name' => getBrowserName($agent)];
}
}
if(!function_exists('getBrowserName')){
function getBrowserName(string $userAgent = ''): string
{
$browsers = [
'/Edg/i' => 'Edge',
'/MSIE|Trident/i' => 'Internet Explorer',
'/Firefox/i' => 'Firefox',
'/OPR|Opera/i' => 'Opera',
'/Chrome|CriOS/i' => 'Chrome',
'/Safari/i' => 'Safari',
];
foreach ($browsers as $pattern => $browser) {
if (preg_match($pattern, $userAgent)) {
return $browser;
}
}
return 'unknown';
}
}
if(! function_exists('get_partner_host')){
function get_partner_host($serverId = 0){
$host = getConfigValue('PARTNER_HOST',$serverId);
$hosts = explode(',',$host);
if(empty($hosts)) return [];
return collect($hosts)->map(function($item){
$item = explode('|',$item);
$notes = Arr::get($item,'2');
return [
'label' => Arr::get($item,'0'),
'value' => Arr::get($item,'1'),
'notes' => $notes?lang($notes):NULL
];
})->toArray();
}
}
if (! function_exists('getConfigValue')) {
function getConfigValue($key, $serverId = 0)
{
if($serverId > 0){
$configVal = ConfigServer::findByFilter([
'configname' => $key,
'server_id' => $serverId,
]);
if($configVal){
return $configVal->configvalue;
}
}
$configVal = ConfigModel::findByFilter([
'name' => $key,
'status' => "1",
]);
if($configVal){
return $configVal->value;
}
return null;
}
}
6. 在控制器中使用注解
```null
<?php namespace appcontroller;
use appannotationLoggable; use thinkResponse;
class User {
title: "用户登录",
logParams: true,
recordIp: true,
recordDevice: true
)]
public function login(): Response
{
return json(['code' => 200, 'msg' => '登录成功']);
}
title: "获取用户信息",
logResult: true,
recordIp: true
)]
public function info(int $id): Response
{
$user = ['id' => $id, 'name' => '张三'];
return json($user);
}
public function publicProfile(int $id): Response
{
return json(['id' => $id, 'public' => true]);
}
}
### 性能优化建议
虽然中间件内部有注解缓存,但全局中间件会对每个请求执行检查。以下优化措施可提升性能:
1. **快速跳过检查**:
null public function handle(Request $request, Closure $next) {
if ($this->shouldSkip($request)) {
return $next($request);
}
}
protected function shouldSkip(Request $request): bool {
if (preg_match('/.(js|css|jpg|png|gif)$/i', $request->pathinfo())) {
return true;
}
$skipRoutes = ['/healthcheck', '/ping'];
if (in_array($request->pathinfo(), $skipRoutes)) {
return true;
}
return false;
}
2. **使用类级别缓存**:
null private static $classAnnotationCache = [];
protected function resolveLoggableAnnotation(string $class, string $action): ?Loggable {
if (!isset(self::$classAnnotationCache[$class])) {
try {
$classReflect = new ReflectionClass($class);
self::$classAnnotationCache[$class] = !empty($classReflect->getAttributes(Loggable::class));
} catch (ReflectionException $e) {
self::$classAnnotationCache[$class] = false;
}
}
if (!self::$classAnnotationCache[$class])) {
return null;
}
}
3. **限制反射范围**:
null protected function resolveLoggableAnnotation(string $class, string $action): ?Loggable {
if (!str_starts_with($class, 'app\controller\')) {
return null;
}
}
### 最佳实践建议
1. **中小型项目**:使用纯注解方案,简化开发
2. **大型高并发项目**:
* 使用路由中间件方案控制范围
* 结合注解缓存和异步队列
3. **混合使用**:
null
public function handle(Request $request, Closure $next) {
if ($request->middleware('loggable')) {
return $this->processWithLog($request, $next);
}
return $this->processWithAnnotation($request, $next);
} ```