Переглянути джерело

add:增加登录日志、行为操作日志记录功能

aiden 3 місяців тому
батько
коміт
7f7457a6c6

+ 59 - 1
app/BaseController.php

@@ -6,6 +6,8 @@ namespace app;
 use think\App;
 use think\exception\ValidateException;
 use think\Validate;
+use app\model\UserBehaviorLogModel;
+use think\facade\Request;
 
 /**
  * 控制器基础类
@@ -77,7 +79,63 @@ abstract class BaseController
     protected function getUserInfo(): array
     {
         return $this->userInfo;
-    }    
+    }
+    
+    /**
+     * 记录操作日志
+     * @param int $status 操作状态
+     * @param array $filterParams 需要过滤的参数键名(如密码等敏感信息)
+     * @return bool
+     */
+    protected function recordBehavior(int $status = UserBehaviorLogModel::STATUS_SUCCESS, array $filterParams = ['password']): bool
+    {
+        try {
+            // 获取用户信息
+            $userInfo = $this->getUserInfo();
+            if (empty($userInfo)) {
+                return false;
+            }
+            
+            // 获取控制器和方法名
+            $controller = Request::controller();
+            $action = Request::action();
+            $behavior = $controller . '/' . $action;
+            
+            // 获取权限配置中的行为描述
+            $permissions = config('permission.permissions');
+            $behaviorText = '';
+            if (isset($permissions[$controller]['actions'][$action])) {
+                $behaviorText = $permissions[$controller]['module'] . '-' . $permissions[$controller]['actions'][$action];
+            } else {
+                $behaviorText = $behavior;
+            }
+            
+            // 获取请求参数并过滤敏感信息
+            $params = Request::param();
+            foreach ($filterParams as $key) {
+                if (isset($params[$key])) {
+                    unset($params[$key]);
+                }
+            }
+            
+            // 构建日志数据
+            $data = [
+                'merchant_id' => $userInfo['merchant_id'] ?? 0,
+                'user_id' => $userInfo['user_id'] ?? 0,
+                'behavior' => $behaviorText,
+                'behavior_desc' => json_encode($params, JSON_UNESCAPED_UNICODE),
+                'behavior_ip' => getClientIp(),
+                'behavior_url' => Request::url(true),
+                'behavior_status' => $status
+            ];
+            
+            // 记录日志
+            return UserBehaviorLogModel::recordBehavior($data);
+        } catch (\Exception $e) {
+            // 记录日志失败不影响业务
+            return false;
+        }
+    }
 
     /**
      * 验证数据

+ 227 - 0
app/controller/BehaviorLog.php

@@ -0,0 +1,227 @@
+<?php
+declare (strict_types = 1);
+
+namespace app\controller;
+
+use app\BaseController;
+use think\facade\Request;
+use app\model\UserBehaviorLogModel;
+use app\model\UserModel;
+
+/**
+ * 操作日志控制器
+ */
+class BehaviorLog extends BaseController
+{
+    /**
+     * 获取操作日志列表
+     */
+    public function list()
+    {
+        $userInfo = $this->request->userInfo;
+        
+        // 获取查询参数
+        $page = Request::get('page', 1, 'intval');
+        $limit = Request::get('limit', 20, 'intval');
+        
+        // 过滤条件
+        $filters = [
+            'user_id' => Request::get('user_id', 0, 'intval'),
+            'behavior' => Request::get('behavior', '', 'trim'),
+            'behavior_status' => Request::get('behavior_status', ''),
+            'behavior_ip' => Request::get('behavior_ip', '', 'trim'),
+            'start_time' => Request::get('start_time', '', 'trim'),
+            'end_time' => Request::get('end_time', '', 'trim'),
+            'keyword' => Request::get('keyword', '', 'trim'),
+        ];
+        
+        try {
+            $result = UserBehaviorLogModel::getBehaviorLogs($userInfo['merchant_id'], $page, $limit, $filters);
+            return json_success($result, '获取成功');
+        } catch (\Exception $e) {
+            return json_error([], '获取操作日志失败:' . $e->getMessage());
+        }
+    }
+    
+    /**
+     * 获取操作统计信息
+     */
+    public function statistics()
+    {
+        $userInfo = $this->request->userInfo;
+        
+        $userId = Request::get('user_id', 0, 'intval');
+        $startDate = Request::get('start_date', '', 'trim');
+        $endDate = Request::get('end_date', '', 'trim');
+        
+        // 如果没有指定日期范围,默认获取最近30天
+        if (empty($startDate)) {
+            $startDate = date('Y-m-d', strtotime('-30 days'));
+        }
+        if (empty($endDate)) {
+            $endDate = date('Y-m-d');
+        }
+        
+        try {
+            $statistics = UserBehaviorLogModel::getBehaviorStatistics($userInfo['merchant_id'], $userId, $startDate, $endDate);
+            
+            return json_success([
+                'statistics' => $statistics,
+                'date_range' => [
+                    'start_date' => $startDate,
+                    'end_date' => $endDate
+                ]
+            ], '获取成功');
+        } catch (\Exception $e) {
+            return json_error([], '获取操作统计失败:' . $e->getMessage());
+        }
+    }
+    
+    /**
+     * 获取用户最近操作记录
+     */
+    public function recentLogs()
+    {
+        $userInfo = $this->request->userInfo;
+        
+        $userId = Request::get('user_id', 0, 'intval');
+        $limit = Request::get('limit', 10, 'intval');
+        
+        // 如果没有指定用户ID,则获取当前用户的操作记录
+        if ($userId == 0) {
+            $userId = $userInfo['user_id'];
+        } else {
+            // 验证用户是否属于当前商户
+            $user = UserModel::where('user_id', $userId)
+                ->where('merchant_id', $userInfo['merchant_id'])
+                ->find();
+            
+            if (!$user) {
+                return json_error([], '用户不存在');
+            }
+        }
+        
+        try {
+            $logs = UserBehaviorLogModel::getRecentBehaviors($userId, $limit);
+            
+            return json_success([
+                'logs' => $logs,
+                'user_id' => $userId
+            ], '获取成功');
+        } catch (\Exception $e) {
+            return json_error([], '获取最近操作记录失败:' . $e->getMessage());
+        }
+    }
+    
+    /**
+     * 获取所有行为类型
+     */
+    public function getBehaviorTypes()
+    {
+        try {
+            $behaviors = UserBehaviorLogModel::getAllBehaviors();
+            return json_success($behaviors, '获取成功');
+        } catch (\Exception $e) {
+            return json_error([], '获取行为类型失败:' . $e->getMessage());
+        }
+    }
+    
+    /**
+     * 获取操作日志详情
+     */
+    public function detail()
+    {
+        $userInfo = $this->request->userInfo;
+        
+        $id = Request::get('id', 0, 'intval');
+        if (!$id) {
+            return json_error([], '日志ID不能为空');
+        }
+        
+        try {
+            $log = UserBehaviorLogModel::where('id', $id)
+                ->where('merchant_id', $userInfo['merchant_id'])
+                ->find();
+                
+            if (!$log) {
+                return json_error([], '日志不存在');
+            }
+            
+            // 获取用户信息
+            $user = UserModel::where('user_id', $log->user_id)->find();
+            if ($user) {
+                $log->user_name = $user->user_name;
+                $log->nick_name = $user->nick_name;
+            }
+            
+            // 格式化数据
+            $log->status_text = $log->behavior_status == UserBehaviorLogModel::STATUS_SUCCESS ? '成功' : '失败';
+            $log->create_time_text = date('Y-m-d H:i:s', $log->create_time);
+            
+            // 解析请求参数
+            try {
+                $log->params = json_decode($log->behavior_desc, true) ?: [];
+            } catch (\Exception $e) {
+                $log->params = [];
+            }
+            
+            return json_success($log, '获取成功');
+        } catch (\Exception $e) {
+            return json_error([], '获取日志详情失败:' . $e->getMessage());
+        }
+    }
+    
+    /**
+     * 导出操作日志
+     */
+    public function export()
+    {
+        $userInfo = $this->request->userInfo;
+        
+        // 过滤条件
+        $filters = [
+            'user_id' => Request::get('user_id', 0, 'intval'),
+            'behavior' => Request::get('behavior', '', 'trim'),
+            'behavior_status' => Request::get('behavior_status', ''),
+            'behavior_ip' => Request::get('behavior_ip', '', 'trim'),
+            'start_time' => Request::get('start_time', '', 'trim'),
+            'end_time' => Request::get('end_time', '', 'trim'),
+            'keyword' => Request::get('keyword', '', 'trim'),
+        ];
+        
+        try {
+            // 获取所有符合条件的数据(不分页)
+            $result = UserBehaviorLogModel::getBehaviorLogs($userInfo['merchant_id'], 1, 100000, $filters);
+            
+            // 生成CSV数据
+            $csvData = "ID,用户名,昵称,操作行为,请求参数,操作IP,操作URL,操作时间,操作状态\n";
+            
+            foreach ($result['list'] as $log) {
+                // 处理请求参数,移除换行符
+                $params = str_replace(["\r", "\n"], ' ', $log['behavior_desc']);
+                
+                $csvData .= sprintf(
+                    "%d,%s,%s,%s,%s,%s,%s,%s,%s\n",
+                    $log['id'],
+                    $log['user_name'],
+                    $log['nick_name'],
+                    $log['behavior'],
+                    $params,
+                    $log['behavior_ip'],
+                    $log['behavior_url'],
+                    $log['create_time_text'],
+                    $log['status_text']
+                );
+            }
+            
+            // 返回CSV数据
+            return response($csvData)
+                ->header(['Content-Type' => 'text/csv; charset=utf-8'])
+                ->header(['Content-Disposition' => 'attachment; filename="behavior_logs_' . date('YmdHis') . '.csv"'])
+                ->header(['Cache-Control' => 'no-cache, must-revalidate']);
+                
+        } catch (\Exception $e) {
+            return json_error([], '导出操作日志失败:' . $e->getMessage());
+        }
+    }
+}

+ 130 - 1
app/controller/User.php

@@ -9,6 +9,7 @@ use think\facade\Request;
 use think\facade\Config;
 use app\model\UserModel;
 use app\model\UserRoleModel;
+use app\model\UserLoginLogModel;
 use app\validate\UserValidate;
 use app\service\IpWhiteListService;
 
@@ -46,12 +47,28 @@ class User extends BaseController
             return json_error([], $checkMessage);
         }
 
+        // 获取客户端信息
+        $clientIp = getClientIp();
+        $userAgent = Request::header('user-agent', '');
+        
         // 查询用户
         $user = UserModel::where('user_name', $userName)->find();
+        
+        // 准备登录日志数据
+        $loginLogData = [
+            'merchant_id' => $user ? $user->merchant_id : 0,
+            'user_id' => $user ? $user->user_id : 0,
+            'login_device' => $userAgent,
+            'login_ip' => $clientIp,
+            'login_status' => UserLoginLogModel::STATUS_FAILED
+        ];
+        
         if ($user && password_verify($password, $user->password)) {
             // 检查IP白名单
-            $clientIp = getClientIp();
             if (!IpWhiteListService::checkIpWhiteList($clientIp, $user->white_list_ip)) {
+                // 记录登录失败日志(IP白名单验证失败)
+                UserLoginLogModel::recordLogin($loginLogData);
+                
                 return json_error([
                     'client_ip' => $clientIp,
                     'white_list_ip' => $user->white_list_ip
@@ -64,9 +81,14 @@ class User extends BaseController
                 'user_role' => $user->user_role
             ]);
             Cookie::set('auth_token', $token, ['expire' => $GLOBALS['cookieExpire'], 'httponly' => true]);
+            
             // 更新登录时间
             $user->login_time = time();
             $user->save();
+            
+            // 记录登录成功日志
+            $loginLogData['login_status'] = UserLoginLogModel::STATUS_SUCCESS;
+            UserLoginLogModel::recordLogin($loginLogData);
 
             return json_success([
                 'user_name' => $user->user_name,
@@ -75,6 +97,9 @@ class User extends BaseController
                 'token' => $token,
             ], $this->message['login']);
         } else {
+            // 记录登录失败日志(密码错误或用户不存在)
+            UserLoginLogModel::recordLogin($loginLogData);
+            
             return json_error([], $this->message['error']);
         }
     }
@@ -352,6 +377,110 @@ class User extends BaseController
         }
     }
     
+    /**
+     * 获取登录日志列表
+     */
+    public function loginLogs()
+    {
+        $userInfo = $this->request->userInfo;
+        
+        // 获取查询参数
+        $page = Request::get('page', 1, 'intval');
+        $limit = Request::get('limit', 20, 'intval');
+        $userId = Request::get('user_id', 0, 'intval');
+        
+        // 过滤条件
+        $filters = [
+            'login_status' => Request::get('login_status', ''),
+            'login_ip' => Request::get('login_ip', '', 'trim'),
+            'start_time' => Request::get('start_time', '', 'trim'),
+            'end_time' => Request::get('end_time', '', 'trim'),
+        ];
+        
+        try {
+            $result = UserLoginLogModel::getLoginLogs($userInfo['merchant_id'], $userId, $page, $limit, $filters);
+            return json_success($result, '获取成功');
+        } catch (\Exception $e) {
+            return json_error([], '获取登录日志失败:' . $e->getMessage());
+        }
+    }
+    
+    /**
+     * 获取登录统计信息
+     */
+    public function loginStatistics()
+    {
+        $userInfo = $this->request->userInfo;
+        
+        $userId = Request::get('user_id', 0, 'intval');
+        $startDate = Request::get('start_date', '', 'trim');
+        $endDate = Request::get('end_date', '', 'trim');
+        
+        // 如果没有指定日期范围,默认获取最近30天
+        if (empty($startDate)) {
+            $startDate = date('Y-m-d', strtotime('-30 days'));
+        }
+        if (empty($endDate)) {
+            $endDate = date('Y-m-d');
+        }
+        
+        try {
+            $statistics = UserLoginLogModel::getLoginStatistics($userInfo['merchant_id'], $userId, $startDate, $endDate);
+            
+            return json_success([
+                'statistics' => $statistics,
+                'date_range' => [
+                    'start_date' => $startDate,
+                    'end_date' => $endDate
+                ]
+            ], '获取成功');
+        } catch (\Exception $e) {
+            return json_error([], '获取登录统计失败:' . $e->getMessage());
+        }
+    }
+    
+    /**
+     * 获取用户最近登录记录
+     */
+    public function recentLoginLogs()
+    {
+        $userInfo = $this->request->userInfo;
+        
+        $userId = Request::get('user_id', 0, 'intval');
+        $limit = Request::get('limit', 10, 'intval');
+        
+        // 如果没有指定用户ID,则获取当前用户的登录记录
+        if ($userId == 0) {
+            $userId = $userInfo['user_id'];
+        } else {
+            // 验证用户是否属于当前商户
+            $user = UserModel::where('user_id', $userId)
+                ->where('merchant_id', $userInfo['merchant_id'])
+                ->find();
+            
+            if (!$user) {
+                return json_error([], '用户不存在');
+            }
+        }
+        
+        try {
+            $logs = UserLoginLogModel::getRecentLogs($userId, $limit);
+            
+            // 格式化日志数据
+            foreach ($logs as &$log) {
+                $log['status_text'] = $log['login_status'] == UserLoginLogModel::STATUS_SUCCESS ? '成功' : '失败';
+                $log['login_time_text'] = date('Y-m-d H:i:s', $log['login_time']);
+            }
+            
+            return json_success([
+                'logs' => $logs,
+                'user_id' => $userId
+            ], '获取成功');
+        } catch (\Exception $e) {
+            return json_error([], '获取最近登录记录失败:' . $e->getMessage());
+        }
+    }
+    
     /**
      * 验证输入数据
      */

+ 192 - 0
app/middleware/BehaviorLogMiddleware.php

@@ -0,0 +1,192 @@
+<?php
+declare (strict_types = 1);
+
+namespace app\middleware;
+
+use app\model\UserBehaviorLogModel;
+use think\facade\Request;
+use Closure;
+
+/**
+ * 操作日志中间件
+ */
+class BehaviorLogMiddleware
+{
+    // 不记录日志的控制器/方法
+    private $excludeActions = [
+        // 查询类操作(不改变数据的操作)
+        'User/list',
+        'User/detail', 
+        'User/loginLogs',
+        'User/loginStatistics',
+        'User/recentLoginLogs',
+        'UserRole/list',
+        'UserRole/detail',
+        'Game/list',
+        'Game/detail',
+        'Game/statistics',
+        'Game/getPlatforms',
+        'Player/list',
+        'Player/detail',
+        'Player/statistics',
+        'Menu/list',
+        'Menu/getUserMenus',
+        'BehaviorLog/list',
+        'BehaviorLog/detail',
+        'BehaviorLog/statistics',
+        'BehaviorLog/recentLogs',
+        'BehaviorLog/getBehaviorTypes',
+        // 登录相关
+        'User/login',
+        'User/logout',
+    ];
+    
+    // 需要过滤的敏感参数
+    private $filterParams = [
+        'password',
+        'token',
+        'auth_token',
+        'secret_key',
+        'private_key',
+    ];
+    
+    /**
+     * 处理请求
+     */
+    public function handle($request, Closure $next)
+    {
+        // 执行请求
+        $response = $next($request);
+        
+        // 请求执行完后记录日志
+        $this->recordBehaviorLog($request, $response);
+        
+        return $response;
+    }
+    
+    /**
+     * 记录操作日志
+     */
+    private function recordBehaviorLog($request, $response)
+    {
+        try {
+            // 检查是否需要记录日志
+            if (!$this->shouldRecord($request)) {
+                return;
+            }
+            
+            // 获取用户信息
+            $userInfo = $request->userInfo ?? [];
+            if (empty($userInfo)) {
+                return; // 没有用户信息则不记录
+            }
+            
+            // 获取控制器和方法名
+            $controller = Request::controller();
+            $action = Request::action();
+            $behavior = $controller . '/' . $action;
+            
+            // 获取权限配置中的行为描述
+            $permissions = config('permission.permissions');
+            $behaviorText = '';
+            if (isset($permissions[$controller]['actions'][$action])) {
+                $behaviorText = $permissions[$controller]['module'] . '-' . $permissions[$controller]['actions'][$action];
+            } else {
+                $behaviorText = $behavior;
+            }
+            
+            // 获取请求参数并过滤敏感信息
+            $params = Request::param();
+            foreach ($this->filterParams as $key) {
+                if (isset($params[$key])) {
+                    unset($params[$key]);
+                }
+            }
+            
+            // 判断操作状态(根据响应状态判断)
+            $status = $this->getOperationStatus($response);
+            
+            // 构建日志数据
+            $data = [
+                'merchant_id' => $userInfo['merchant_id'] ?? 0,
+                'user_id' => $userInfo['user_id'] ?? 0,
+                'behavior' => $behaviorText,
+                'behavior_desc' => json_encode($params, JSON_UNESCAPED_UNICODE),
+                'behavior_ip' => getClientIp(),
+                'behavior_url' => Request::pathinfo(),
+                'behavior_status' => $status
+            ];
+            
+            // 异步记录日志(避免影响响应性能)
+            $this->asyncRecordLog($data);
+            
+        } catch (\Exception $e) {
+            // 记录日志失败不影响业务
+            // 可以在这里记录到错误日志中
+        }
+    }
+    
+    /**
+     * 判断是否需要记录日志
+     */
+    private function shouldRecord($request): bool
+    {
+        // 只记录POST、PUT、DELETE请求
+        $method = Request::method();
+        if (!in_array($method, ['POST', 'PUT', 'DELETE', 'PATCH'])) {
+            return false;
+        }
+        
+        // 检查是否在排除列表中
+        $controller = Request::controller();
+        $action = Request::action();
+        $currentAction = $controller . '/' . $action;
+        
+        if (in_array($currentAction, $this->excludeActions)) {
+            return false;
+        }
+        
+        // 检查权限配置中是否存在该操作
+        $permissions = config('permission.permissions');
+        if (!isset($permissions[$controller]['actions'][$action])) {
+            return false; // 权限配置中不存在的操作不记录
+        }
+        
+        return true;
+    }
+    
+    /**
+     * 根据响应判断操作状态
+     */
+    private function getOperationStatus($response): int
+    {
+        try {
+            // 获取响应内容
+            $content = $response->getContent();
+            $data = json_decode($content, true);
+            
+            // 根据响应的code字段判断
+            if (isset($data['code'])) {
+                return $data['code'] == 200 ? UserBehaviorLogModel::STATUS_SUCCESS : UserBehaviorLogModel::STATUS_FAILED;
+            }
+            
+            // 根据HTTP状态码判断
+            $statusCode = $response->getCode();
+            return $statusCode >= 200 && $statusCode < 300 ? UserBehaviorLogModel::STATUS_SUCCESS : UserBehaviorLogModel::STATUS_FAILED;
+            
+        } catch (\Exception $e) {
+            // 默认为失败
+            return UserBehaviorLogModel::STATUS_FAILED;
+        }
+    }
+    
+    /**
+     * 异步记录日志
+     */
+    private function asyncRecordLog(array $data)
+    {
+        // 这里可以使用队列异步处理
+        // 暂时直接记录
+        UserBehaviorLogModel::recordBehavior($data);
+    }
+}

+ 248 - 0
app/model/UserBehaviorLogModel.php

@@ -0,0 +1,248 @@
+<?php
+declare (strict_types = 1);
+
+namespace app\model;
+
+use think\Model;
+
+/**
+ * 用户操作日志模型
+ */
+class UserBehaviorLogModel extends Model
+{
+    // 设置表名
+    protected $table = 'merchant_user_behavior_log';
+    
+    // 设置主键
+    protected $pk = 'id';
+    
+    // 操作状态常量
+    const STATUS_FAILED = 0;  // 操作失败
+    const STATUS_SUCCESS = 1; // 操作成功
+    
+    /**
+     * 记录操作日志
+     * @param array $data 日志数据
+     * @return bool
+     */
+    public static function recordBehavior(array $data): bool
+    {
+        try {
+            $log = new self();
+            $log->merchant_id = $data['merchant_id'] ?? 0;
+            $log->user_id = $data['user_id'] ?? 0;
+            $log->behavior = $data['behavior'] ?? '';
+            $log->behavior_desc = $data['behavior_desc'] ?? '';
+            $log->behavior_ip = $data['behavior_ip'] ?? '';
+            $log->behavior_url = $data['behavior_url'] ?? '';
+            $log->create_time = time();
+            $log->behavior_status = $data['behavior_status'] ?? self::STATUS_SUCCESS;
+            
+            return $log->save();
+        } catch (\Exception $e) {
+            // 记录日志失败不影响业务流程
+            return false;
+        }
+    }
+    
+    /**
+     * 获取操作日志列表
+     * @param int $merchantId 商户ID
+     * @param int $page 页码
+     * @param int $limit 每页数量
+     * @param array $filters 过滤条件
+     * @return array
+     */
+    public static function getBehaviorLogs(int $merchantId, int $page = 1, int $limit = 20, array $filters = []): array
+    {
+        $where = [
+            ['merchant_id', '=', $merchantId]
+        ];
+        
+        // 用户ID筛选
+        if (!empty($filters['user_id'])) {
+            $where[] = ['user_id', '=', $filters['user_id']];
+        }
+        
+        // 行为类型筛选
+        if (!empty($filters['behavior'])) {
+            $where[] = ['behavior', '=', $filters['behavior']];
+        }
+        
+        // 操作状态筛选
+        if (isset($filters['behavior_status']) && $filters['behavior_status'] !== '') {
+            $where[] = ['behavior_status', '=', $filters['behavior_status']];
+        }
+        
+        // IP地址筛选
+        if (!empty($filters['behavior_ip'])) {
+            $where[] = ['behavior_ip', 'like', '%' . $filters['behavior_ip'] . '%'];
+        }
+        
+        // 时间范围筛选
+        if (!empty($filters['start_time'])) {
+            $where[] = ['create_time', '>=', strtotime($filters['start_time'])];
+        }
+        if (!empty($filters['end_time'])) {
+            $where[] = ['create_time', '<=', strtotime($filters['end_time'])];
+        }
+        
+        // 关键词搜索(在行为描述中搜索)
+        if (!empty($filters['keyword'])) {
+            $where[] = ['behavior_desc', 'like', '%' . $filters['keyword'] . '%'];
+        }
+        
+        $query = self::where($where);
+        
+        $total = $query->count();
+        
+        $list = $query->field('id, merchant_id, user_id, behavior, behavior_desc, behavior_ip, behavior_url, create_time, behavior_status')
+            ->order('id', 'desc')
+            ->page($page, $limit)
+            ->select();
+            
+        // 获取相关用户信息
+        if ($list->count() > 0) {
+            $userIds = array_unique(array_column($list->toArray(), 'user_id'));
+            $users = UserModel::whereIn('user_id', $userIds)
+                ->field('user_id, user_name, nick_name')
+                ->select()
+                ->toArray();
+            
+            $userMap = [];
+            foreach ($users as $user) {
+                $userMap[$user['user_id']] = $user;
+            }
+            
+            // 添加用户信息和格式化数据
+            foreach ($list as &$log) {
+                $log['user_name'] = $userMap[$log['user_id']]['user_name'] ?? '';
+                $log['nick_name'] = $userMap[$log['user_id']]['nick_name'] ?? '';
+                $log['status_text'] = $log['behavior_status'] == self::STATUS_SUCCESS ? '成功' : '失败';
+            }
+        }
+        
+        return [
+            'list' => $list,
+            'total' => $total,
+            'page' => $page,
+            'limit' => $limit
+        ];
+    }
+    
+    /**
+     * 获取用户最近操作记录
+     * @param int $userId 用户ID
+     * @param int $limit 获取数量
+     * @return array
+     */
+    public static function getRecentBehaviors(int $userId, int $limit = 10): array
+    {
+        $list = self::where('user_id', $userId)
+            ->field('behavior, behavior_desc, behavior_ip, behavior_url, create_time, behavior_status')
+            ->order('id', 'desc')
+            ->limit($limit)
+            ->select()
+            ->toArray();
+            
+        // 格式化数据
+        foreach ($list as &$log) {
+            $log['status_text'] = $log['behavior_status'] == self::STATUS_SUCCESS ? '成功' : '失败';
+            $log['create_time_text'] = date('Y-m-d H:i:s', $log['create_time']);
+            
+            // 解析请求参数
+            try {
+                $log['params'] = json_decode($log['behavior_desc'], true) ?: [];
+            } catch (\Exception $e) {
+                $log['params'] = [];
+            }
+        }
+        
+        return $list;
+    }
+    
+    /**
+     * 获取操作统计信息
+     * @param int $merchantId 商户ID
+     * @param int $userId 用户ID(可选)
+     * @param string $startDate 开始日期
+     * @param string $endDate 结束日期
+     * @return array
+     */
+    public static function getBehaviorStatistics(int $merchantId, int $userId = 0, string $startDate = '', string $endDate = ''): array
+    {
+        $where = [
+            ['merchant_id', '=', $merchantId]
+        ];
+        
+        if ($userId > 0) {
+            $where[] = ['user_id', '=', $userId];
+        }
+        
+        if ($startDate) {
+            $where[] = ['create_time', '>=', strtotime($startDate)];
+        }
+        
+        if ($endDate) {
+            $where[] = ['create_time', '<=', strtotime($endDate . ' 23:59:59')];
+        }
+        
+        // 总操作次数
+        $totalCount = self::where($where)->count();
+        
+        // 成功次数
+        $successCount = self::where($where)
+            ->where('behavior_status', self::STATUS_SUCCESS)
+            ->count();
+            
+        // 失败次数
+        $failedCount = self::where($where)
+            ->where('behavior_status', self::STATUS_FAILED)
+            ->count();
+            
+        // 活跃用户数
+        $activeUsers = self::where($where)
+            ->group('user_id')
+            ->count();
+            
+        // 按行为类型统计
+        $behaviorStats = self::where($where)
+            ->field('behavior, count(*) as count')
+            ->group('behavior')
+            ->select()
+            ->toArray();
+            
+        // 行为统计已经是权限配置中的描述,无需额外处理
+        
+        return [
+            'total_count' => $totalCount,
+            'success_count' => $successCount,
+            'failed_count' => $failedCount,
+            'success_rate' => $totalCount > 0 ? round($successCount / $totalCount * 100, 2) : 0,
+            'active_users' => $activeUsers,
+            'behavior_stats' => $behaviorStats
+        ];
+    }
+    
+    /**
+     * 获取所有行为类型(从权限配置中获取)
+     * @return array
+     */
+    public static function getAllBehaviors(): array
+    {
+        $behaviors = [];
+        $permissions = config('permission.permissions');
+        
+        foreach ($permissions as $controller => $moduleInfo) {
+            $module = $moduleInfo['module'] ?? $controller;
+            foreach ($moduleInfo['actions'] ?? [] as $action => $desc) {
+                $behaviors[] = [
+                    'value' => $module . '-' . $desc,
+                    'label' => $module . '-' . $desc
+                ];
+            }
+        }
+        
+        return $behaviors;
+    }
+}

+ 190 - 0
app/model/UserLoginLogModel.php

@@ -0,0 +1,190 @@
+<?php
+declare (strict_types = 1);
+
+namespace app\model;
+
+use think\Model;
+
+/**
+ * 用户登录日志模型
+ */
+class UserLoginLogModel extends Model
+{
+    // 设置表名
+    protected $table = 'merchant_user_login_log';
+    
+    // 设置主键
+    protected $pk = 'id';
+    
+    // 登录状态常量
+    const STATUS_FAILED = 0;  // 登录失败
+    const STATUS_SUCCESS = 1; // 登录成功
+    
+    /**
+     * 记录登录日志
+     * @param array $data 日志数据
+     * @return bool
+     */
+    public static function recordLogin(array $data): bool
+    {
+        try {
+            $log = new self();
+            $log->merchant_id = $data['merchant_id'] ?? 0;
+            $log->user_id = $data['user_id'] ?? 0;
+            $log->login_device = $data['login_device'] ?? '';
+            $log->login_ip = $data['login_ip'] ?? '';
+            $log->login_time = time();
+            $log->login_status = $data['login_status'] ?? self::STATUS_FAILED;
+            
+            return $log->save();
+        } catch (\Exception $e) {
+            // 记录日志失败不影响登录流程
+            return false;
+        }
+    }
+    
+    /**
+     * 获取登录日志列表
+     * @param int $merchantId 商户ID
+     * @param int $userId 用户ID(可选)
+     * @param int $page 页码
+     * @param int $limit 每页数量
+     * @param array $filters 过滤条件
+     * @return array
+     */
+    public static function getLoginLogs(int $merchantId, int $userId = 0, int $page = 1, int $limit = 20, array $filters = []): array
+    {
+        $where = [
+            ['merchant_id', '=', $merchantId]
+        ];
+        
+        // 如果指定了用户ID
+        if ($userId > 0) {
+            $where[] = ['user_id', '=', $userId];
+        }
+        
+        // 登录状态筛选
+        if (isset($filters['login_status']) && $filters['login_status'] !== '') {
+            $where[] = ['login_status', '=', $filters['login_status']];
+        }
+        
+        // IP地址筛选
+        if (!empty($filters['login_ip'])) {
+            $where[] = ['login_ip', 'like', '%' . $filters['login_ip'] . '%'];
+        }
+        
+        // 时间范围筛选
+        if (!empty($filters['start_time'])) {
+            $where[] = ['login_time', '>=', strtotime($filters['start_time'])];
+        }
+        if (!empty($filters['end_time'])) {
+            $where[] = ['login_time', '<=', strtotime($filters['end_time'])];
+        }
+        
+        $query = self::where($where);
+        
+        $total = $query->count();
+        
+        $list = $query->field('id, merchant_id, user_id, login_device, login_ip, login_time, login_status')
+            ->order('id', 'desc')
+            ->page($page, $limit)
+            ->select();
+            
+        // 获取相关用户信息
+        if ($list->count() > 0) {
+            $userIds = array_unique(array_column($list->toArray(), 'user_id'));
+            $users = UserModel::whereIn('user_id', $userIds)
+                ->field('user_id, user_name, nick_name')
+                ->select()
+                ->toArray();
+            
+            $userMap = [];
+            foreach ($users as $user) {
+                $userMap[$user['user_id']] = $user;
+            }
+            
+            // 添加用户信息到日志记录
+            foreach ($list as &$log) {
+                $log['user_name'] = $userMap[$log['user_id']]['user_name'] ?? '';
+                $log['nick_name'] = $userMap[$log['user_id']]['nick_name'] ?? '';
+                $log['status_text'] = $log['login_status'] == self::STATUS_SUCCESS ? '成功' : '失败';
+            }
+        }
+        
+        return [
+            'list' => $list,
+            'total' => $total,
+            'page' => $page,
+            'limit' => $limit
+        ];
+    }
+    
+    /**
+     * 获取用户最近登录记录
+     * @param int $userId 用户ID
+     * @param int $limit 获取数量
+     * @return array
+     */
+    public static function getRecentLogs(int $userId, int $limit = 10): array
+    {
+        return self::where('user_id', $userId)
+            ->field('login_device, login_ip, login_time, login_status')
+            ->order('id', 'desc')
+            ->limit($limit)
+            ->select()
+            ->toArray();
+    }
+    
+    /**
+     * 获取登录统计信息
+     * @param int $merchantId 商户ID
+     * @param int $userId 用户ID(可选)
+     * @param string $startDate 开始日期
+     * @param string $endDate 结束日期
+     * @return array
+     */
+    public static function getLoginStatistics(int $merchantId, int $userId = 0, string $startDate = '', string $endDate = ''): array
+    {
+        $where = [
+            ['merchant_id', '=', $merchantId]
+        ];
+        
+        if ($userId > 0) {
+            $where[] = ['user_id', '=', $userId];
+        }
+        
+        if ($startDate) {
+            $where[] = ['login_time', '>=', strtotime($startDate)];
+        }
+        
+        if ($endDate) {
+            $where[] = ['login_time', '<=', strtotime($endDate . ' 23:59:59')];
+        }
+        
+        // 总登录次数
+        $totalCount = self::where($where)->count();
+        
+        // 成功次数
+        $successCount = self::where($where)
+            ->where('login_status', self::STATUS_SUCCESS)
+            ->count();
+            
+        // 失败次数
+        $failedCount = self::where($where)
+            ->where('login_status', self::STATUS_FAILED)
+            ->count();
+            
+        // 独立IP数
+        $uniqueIps = self::where($where)
+            ->group('login_ip')
+            ->count();
+            
+        return [
+            'total_count' => $totalCount,
+            'success_count' => $successCount,
+            'failed_count' => $failedCount,
+            'success_rate' => $totalCount > 0 ? round($successCount / $totalCount * 100, 2) : 0,
+            'unique_ips' => $uniqueIps
+        ];
+    }
+}

+ 17 - 1
config/permission.php

@@ -75,10 +75,26 @@ return [
             'module' => '账号管理',
             'actions' => [
                 'list' => '账号列表',
-                'create' => '创建账号',
+                'createUser' => '创建账号',
                 'update' => '编辑账号',
                 'delete' => '删除账号',
                 'detail' => '查看账号详情',
+                'loginLogs' => '查看登录日志',
+                'loginStatistics' => '查看登录统计',
+                'recentLoginLogs' => '查看最近登录记录',
+            ]
+        ],
+        
+        // 操作日志管理模块
+        'BehaviorLog' => [
+            'module' => '操作日志',
+            'actions' => [
+                'list' => '查看操作日志列表',
+                'detail' => '查看操作日志详情',
+                'statistics' => '查看操作统计',
+                'recentLogs' => '查看最近操作记录',
+                'getBehaviorTypes' => '获取行为类型',
+                'export' => '导出操作日志',
             ]
         ],
         

+ 19 - 5
route/app.php

@@ -19,7 +19,11 @@ Route::group('user', function () {
     Route::get('detail', 'User/detail');
     Route::post('update', 'User/update');
     Route::post('delete', 'User/delete');
-})->middleware(\app\middleware\AuthMiddleware::class);
+    // 登录日志相关
+    Route::get('login_logs', 'User/loginLogs');
+    Route::get('login_statistics', 'User/loginStatistics');
+    Route::get('recent_login_logs', 'User/recentLoginLogs');
+})->middleware([\app\middleware\AuthMiddleware::class, \app\middleware\BehaviorLogMiddleware::class]);
 
 // 角色相关路由
 Route::group('user_role', function () {
@@ -28,13 +32,13 @@ Route::group('user_role', function () {
     Route::post('create', 'UserRole/create');
     Route::post('update', 'UserRole/update');
     Route::post('delete', 'UserRole/delete');
-})->middleware(\app\middleware\AuthMiddleware::class);
+})->middleware([\app\middleware\AuthMiddleware::class, \app\middleware\BehaviorLogMiddleware::class]);
 
 // 菜单-权限相关路由
 Route::group('menu', function () {
     Route::get('get_user_menus', 'Menu/getUserMenus');
     Route::get('get_all_permissions', 'Menu/getAllPermissions');
-})->middleware(\app\middleware\AuthMiddleware::class);
+})->middleware([\app\middleware\AuthMiddleware::class, \app\middleware\BehaviorLogMiddleware::class]);
 
 // 玩家相关路由
 Route::group('player', function () {
@@ -44,7 +48,7 @@ Route::group('player', function () {
     Route::post('update_adjust_status', 'Player/updateAdjustStatus');
     Route::get('statistics', 'Player/statistics');
     Route::get('export', 'Player/export');
-})->middleware(\app\middleware\AuthMiddleware::class);
+})->middleware([\app\middleware\AuthMiddleware::class, \app\middleware\BehaviorLogMiddleware::class]);
 
 // 游戏相关路由
 Route::group('game', function () {
@@ -55,4 +59,14 @@ Route::group('game', function () {
     Route::get('statistics', 'Game/statistics');
     Route::get('get_platforms', 'Game/getPlatforms');
     Route::get('export', 'Game/export');
-})->middleware(\app\middleware\AuthMiddleware::class);
+})->middleware([\app\middleware\AuthMiddleware::class, \app\middleware\BehaviorLogMiddleware::class]);
+
+// 操作日志相关路由
+Route::group('behavior_log', function () {
+    Route::get('list', 'BehaviorLog/list');
+    Route::get('detail', 'BehaviorLog/detail');
+    Route::get('statistics', 'BehaviorLog/statistics');
+    Route::get('recent_logs', 'BehaviorLog/recentLogs');
+    Route::get('get_behavior_types', 'BehaviorLog/getBehaviorTypes');
+    Route::get('export', 'BehaviorLog/export');
+})->middleware([\app\middleware\AuthMiddleware::class, \app\middleware\BehaviorLogMiddleware::class]);