insertGetId([ 'action' => $action, 'operator_id' => $operatorId, 'operator_name' => $operatorName, 'target_type' => $targetType, 'target_id' => $targetId, 'target_desc' => $targetDesc ?: self::buildTargetDesc($targetType, $targetId), 'client_ip' => $clientIp, 'user_agent' => mb_substr($userAgent, 0, 512), 'request_id' => $requestId, 'extra' => empty($extra) ? null : json_encode($extra, JSON_UNESCAPED_UNICODE), 'created_at' => $createdAt, ]); return $id; } catch (\Throwable $e) { // 审计日志写入失败不阻断主业务流程,但记录警告 BaseService::log('AuditService::log failed', [ 'action' => $action, 'targetType' => $targetType, 'targetId' => $targetId, 'error' => $e->getMessage(), ], 'warning'); return false; } } // ======================== // 便捷包装方法(核销操作) // ======================== /** * 记录核销操作 */ public static function logVerify($ticketId, $ticketCode, $verifierId, $verifierName, $result, $oldStatus) { return self::log( self::ACTION_VERIFY, self::TARGET_TICKET, $ticketId, [ 'ticket_code' => $ticketCode, 'verifier_id' => $verifierId, 'verifier' => $verifierName, 'old_status' => $oldStatus, 'result' => $result, ], "票码: {$ticketCode}" ); } /** * 记录导出操作 */ public static function logExport($goodsId, $filter, $count) { return self::log( self::ACTION_EXPORT, self::TARGET_GOODS, $goodsId, [ 'filter' => $filter, 'count' => $count, ], $goodsId > 0 ? "商品ID: {$goodsId}" : '全量导出' ); } // ======================== // 查询接口(供管理后台使用) // ======================== /** * 查询审计日志(分页) * * @param array $params 查询参数:action, operator_id, target_type, target_id, date_from, date_to, page, limit * @return array */ public static function search($params = []) { $where = []; if (!empty($params['action'])) { $where[] = ['action', '=', $params['action']]; } if (!empty($params['operator_id'])) { $where[] = ['operator_id', '=', intval($params['operator_id'])]; } if (!empty($params['target_type'])) { $where[] = ['target_type', '=', $params['target_type']]; } if (!empty($params['target_id'])) { $where[] = ['target_id', '=', intval($params['target_id'])]; } if (!empty($params['date_from'])) { $where[] = ['created_at', '>=', strtotime($params['date_from'])]; } if (!empty($params['date_to'])) { $where[] = ['created_at', '<=', strtotime($params['date_to'] . ' 23:59:59')]; } $page = max(1, intval($params['page'] ?? 1)); $pageSize = min(100, max(10, intval($params['limit'] ?? 20))); $result = \Db::name(BaseService::table('audit_log')) ->where($where) ->order('id', 'desc') ->paginate($pageSize) ->toArray(); // JSON 解析 extra 字段 if (!empty($result['data'])) { foreach ($result['data'] as &$row) { if (!empty($row['extra'])) { $row['extra'] = json_decode($row['extra'], true); } } unset($row); } return $result; } // ======================== // 内部工具方法 // ======================== /** * 获取当前操作用户 ID */ private static function getOperatorId() { // ShopXO admin session: $this->admin['id'] 在控制器中 // 在服务层通过 session() 或 app() 获取 $admin = session('admin'); return isset($admin['id']) ? intval($admin['id']) : 0; } /** * 获取当前操作用户名称 */ private static function getOperatorName() { $admin = session('admin'); return $admin['username'] ?? ($admin['name'] ?? ''); } /** * 获取客户端真实 IP */ private static function getClientIp() { $ip = request()->ip(0, true); // true = 穿透代理 return $ip ?: ''; } /** * 获取 User-Agent */ private static function getUserAgent() { return request()->header('user-agent', ''); } /** * 获取或创建请求追踪 ID(用于关联同一 HTTP 请求中的多个操作) */ private static function getOrCreateRequestId() { static $requestId = null; if ($requestId === null) { $requestId = session('vr_ticket_request_id'); if (empty($requestId)) { $requestId = self::generateRequestId(); session('vr_ticket_request_id', $requestId); } } return $requestId; } /** * 生成唯一请求 ID */ private static function generateRequestId() { return sprintf( '%s-%s-%04x-%04x-%04x', date('YmdHis'), substr(md5(uniqid((string) mt_rand(), true)), 0, 8), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); } /** * 根据对象类型和 ID 构建描述文本 */ private static function buildTargetDesc($targetType, $targetId) { switch ($targetType) { case self::TARGET_TICKET: $ticket = \Db::name(BaseService::table('tickets'))->where('id', $targetId)->find(); return $ticket ? "票码: {$ticket['ticket_code']}" : "票ID: {$targetId}"; case self::TARGET_VERIFIER: $verifier = \Db::name(BaseService::table('verifiers'))->where('id', $targetId)->find(); return $verifier ? "核销员: {$verifier['name']}" : "核销员ID: {$targetId}"; case self::TARGET_TEMPLATE: $template = \Db::name(BaseService::table('seat_templates'))->where('id', $targetId)->find(); return $template ? "模板: {$template['name']}" : "模板ID: {$targetId}"; default: return "{$targetType}:{$targetId}"; } } }