WordPress日志记录与访客人数

效果图

image

image

image

 

日志抓包如果过于太多,在这里取消几个选择,只指定需要的那几个抓取的页面即可

image

 

下面是插件目录以及源码

image

 

 

 cn-user-ops-logger.php文件

<?php
/**
 * Plugin Name: 日志记录
 * Plugin URI: https://www.cnblogs.com/Boboschen/p/19683405
 * Description: 记录普通用户前台操作、WooCommerce 购物车/结账/支付过程、失败原因与订单状态变化;提供中文后台日志管理、访客在线统计、国家来源分析、文件归档与批量删除功能。默认不记录管理员行为,前台静默运行,不向用户显示额外提示。
 * Version: 2.4.0
 * Author: 卡卡
 * Text Domain: User Log
 */

if (!defined('ABSPATH')) exit;

define('CUOL_VERSION', '2.4.0');
define('CUOL_FILE', __FILE__);
define('CUOL_PATH', plugin_dir_path(__FILE__));
define('CUOL_URL', plugin_dir_url(__FILE__));
define('CUOL_OPTION_RETENTION_DAYS', 'cuol_retention_days');
define('CUOL_OPTION_TRACKING_SETTINGS', 'cuol_tracking_settings');
define('CUOL_VISITOR_TIMEOUT_MINUTES', 5);

require_once CUOL_PATH . 'includes/class-cuol-file-store.php';
require_once CUOL_PATH . 'includes/class-cuol-logger.php';
require_once CUOL_PATH . 'includes/class-cuol-visitor.php';
require_once CUOL_PATH . 'includes/class-cuol-admin.php';

class CUOL_Plugin {
    public function __construct() {
        register_activation_hook(CUOL_FILE, array($this, 'activate'));
        register_uninstall_hook(CUOL_FILE, array('CUOL_Plugin', 'uninstall'));
        add_action('plugins_loaded', array($this, 'init'));
    }

    public function activate() {
        CUOL_Logger::create_table();
        CUOL_Visitor::create_table();
        CUOL_File_Store::ensure_directories();
        if (get_option(CUOL_OPTION_RETENTION_DAYS, null) === null) add_option(CUOL_OPTION_RETENTION_DAYS, 30);
        if (get_option(CUOL_OPTION_TRACKING_SETTINGS, null) === null) add_option(CUOL_OPTION_TRACKING_SETTINGS, CUOL_Logger::default_tracking_settings());
        if (!wp_next_scheduled('cuol_daily_cleanup_event')) wp_schedule_event(time() + HOUR_IN_SECONDS, 'daily', 'cuol_daily_cleanup_event');
    }

    public static function uninstall() {
        global $wpdb;
        $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}cuol_logs");
        $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}cuol_visitors");
        delete_option(CUOL_OPTION_RETENTION_DAYS);
        delete_option(CUOL_OPTION_TRACKING_SETTINGS);
        wp_clear_scheduled_hook('cuol_daily_cleanup_event');
    }

    public function init() {
        CUOL_File_Store::ensure_directories();
        CUOL_Logger::init();
        CUOL_Visitor::init();
        if (is_admin()) CUOL_Admin::init();
        $this->register_hooks();
    }

    private function register_hooks() {
        add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets'));
        add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_runtime_assets'));
        add_action('admin_bar_menu', array($this, 'add_admin_bar_online_node'), 100);
        add_action('template_redirect', array($this, 'track_front_visitor_request'), 1);
        add_action('admin_init', array($this, 'track_admin_request'), 1);
        add_action('template_redirect', array($this, 'log_front_page_open'), 20);

        add_action('wp_ajax_cuol_log_checkout_issue', array($this, 'ajax_log_checkout_issue'));
        add_action('wp_ajax_nopriv_cuol_log_checkout_issue', array($this, 'ajax_log_checkout_issue'));
        add_action('wp_ajax_cuol_ping_visitor', array('CUOL_Visitor', 'ajax_ping'));
        add_action('wp_ajax_nopriv_cuol_ping_visitor', array('CUOL_Visitor', 'ajax_ping'));
        add_action('wp_ajax_cuol_get_live_counts', array('CUOL_Visitor', 'ajax_live_counts'));

        add_action('cuol_daily_cleanup_event', array('CUOL_Logger', 'cleanup_old_logs'));
        add_action('cuol_daily_cleanup_event', array('CUOL_Visitor', 'cleanup_old_records'));
        add_action('cuol_daily_cleanup_event', array('CUOL_File_Store', 'cleanup_old_files'));

        add_action('wp_login', array($this, 'log_login'), 10, 2);
        add_action('wp_logout', array($this, 'log_logout'));
        add_action('user_register', array($this, 'log_register'));
        add_action('profile_update', array($this, 'log_profile_update'), 10, 2);
        add_action('comment_post', array($this, 'log_comment_post'), 10, 3);

        add_action('woocommerce_add_to_cart', array($this, 'log_add_to_cart'), 10, 6);
        add_action('woocommerce_remove_cart_item', array($this, 'log_remove_cart_item'), 10, 2);
        add_action('woocommerce_applied_coupon', array($this, 'log_applied_coupon'));
        add_action('woocommerce_removed_coupon', array($this, 'log_removed_coupon'));
        add_action('woocommerce_after_checkout_validation', array($this, 'log_checkout_validation'), 10, 2);
        add_action('woocommerce_checkout_order_processed', array($this, 'log_order_processed'), 10, 3);
        add_action('woocommerce_payment_complete', array($this, 'log_payment_complete'));
        add_action('woocommerce_order_status_changed', array($this, 'log_order_status_changed'), 10, 4);
        add_action('woocommerce_order_status_failed', array($this, 'log_order_failed'), 10, 2);
        add_action('woocommerce_order_status_cancelled', array($this, 'log_order_cancelled'), 10, 2);
        add_action('woocommerce_thankyou', array($this, 'log_thankyou_result'));
        add_action('woocommerce_checkout_order_exception', array($this, 'log_checkout_exception'), 10, 2);
        add_action('woocommerce_after_checkout_validation', array($this, 'capture_checkout_notices'), 999, 2);
    }

    private function get_runtime_localize() {
        return array(
            'ajax_url' => admin_url('admin-ajax.php'),
            'nonce'    => wp_create_nonce('cuol_runtime_nonce'),
            'page_url' => esc_url_raw(CUOL_Logger::current_url()),
            'is_admin' => is_admin() ? 1 : 0,
            'refresh'  => is_admin() ? 5 : 10,
        );
    }

    public function enqueue_frontend_assets() {
        wp_enqueue_script('cuol-runtime', CUOL_URL . 'assets/js/runtime.js', array('jquery'), CUOL_VERSION, true);
        wp_localize_script('cuol-runtime', 'CUOL_Runtime', $this->get_runtime_localize());

        if (function_exists('is_checkout') && is_checkout() && !is_order_received_page()) {
            wp_enqueue_script('cuol-checkout-monitor', CUOL_URL . 'assets/js/checkout-monitor.js', array('jquery', 'cuol-runtime'), CUOL_VERSION, true);
            wp_localize_script('cuol-checkout-monitor', 'CUOL_Monitor', array(
                'ajax_url' => admin_url('admin-ajax.php'),
                'nonce'    => wp_create_nonce('cuol_checkout_log_nonce'),
                'page_url' => esc_url_raw(CUOL_Logger::current_url()),
            ));
        }
    }

    public function enqueue_admin_runtime_assets() {
        wp_enqueue_script('cuol-runtime', CUOL_URL . 'assets/js/runtime.js', array('jquery'), CUOL_VERSION, true);
        wp_localize_script('cuol-runtime', 'CUOL_Runtime', $this->get_runtime_localize());
    }

    public function add_admin_bar_online_node($wp_admin_bar) {
        if (!current_user_can('manage_woocommerce')) return;
        $count = CUOL_Visitor::get_online_count(false);
        $wp_admin_bar->add_node(array(
            'id'    => 'cuol-online-count',
            'title' => '在线人数:<span id="cuol-adminbar-online-count">' . intval($count) . '</span>',
            'href'  => admin_url('admin.php?page=cuol-visitors'),
            'meta'  => array('class' => 'cuol-adminbar-online')
        ));
    }


    public function track_front_visitor_request() {
        if (is_admin() || wp_doing_ajax() || (defined('REST_REQUEST') && REST_REQUEST)) return;
        CUOL_Visitor::track_current_request(false);
    }

    public function track_admin_request() {
        if (!is_admin() || wp_doing_ajax()) return;
        CUOL_Visitor::track_current_request(true, true);
    }

    public function log_front_page_open() {
        if (is_admin() || wp_doing_ajax() || (defined('REST_REQUEST') && REST_REQUEST)) return;
        if (defined('DOING_CRON') && DOING_CRON) return;
        if (is_feed() || is_trackback() || is_preview()) return;
        $current_url = CUOL_Logger::current_url();
        if (CUOL_Logger::should_ignore_request($current_url)) return;
        $module = '网站访问';
        if (function_exists('is_shop') && is_shop()) $module = '商城';
        elseif (function_exists('is_product') && is_product()) $module = '商品';
        elseif (function_exists('is_product_category') && is_product_category()) $module = '商品分类';
        elseif (is_singular('post')) $module = '文章';
        elseif (is_page()) $module = '页面';
        elseif (is_category() || is_tag() || is_tax()) $module = '内容归档';

        $object_type = '';
        $object_id = 0;
        if (is_singular()) {
            $object_type = get_post_type() ?: 'post';
            $object_id = get_queried_object_id();
        }

        CUOL_Logger::log(array(
            'module'      => $module,
            'action_type' => '访问',
            'action_name' => '打开页面',
            'severity'    => 'info',
            'location'    => CUOL_Logger::detect_location($current_url),
            'object_type' => $object_type,
            'object_id'   => $object_id,
            'details'     => '用户打开了一个网站页面。',
            'extra_data'  => array(
                'title'   => wp_get_document_title(),
                'referer' => isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : '',
                'url'     => $current_url,
            ),
        ));
    }

    public function ajax_log_checkout_issue() {
        check_ajax_referer('cuol_checkout_log_nonce', 'nonce');
        $order_id = absint($_POST['order_id'] ?? 0);
        CUOL_Logger::log(array(
            'module'      => '结账/支付',
            'action_type' => '结账异常',
            'action_name' => sanitize_text_field(wp_unslash($_POST['action_name'] ?? '前端结账异常')),
            'severity'    => sanitize_text_field(wp_unslash($_POST['severity'] ?? 'error')),
            'location'    => sanitize_text_field(wp_unslash($_POST['location'] ?? '结账页')),
            'details'     => wp_kses_post(wp_unslash($_POST['details'] ?? '')),
            'object_type' => $order_id ? 'order' : '',
            'object_id'   => $order_id,
            'extra_data'  => array(
                'order_id'       => $order_id,
                'request_url'    => esc_url_raw(wp_unslash($_POST['request_url'] ?? '')),
                'page_url'       => esc_url_raw(wp_unslash($_POST['page_url'] ?? '')),
                'http_status'    => sanitize_text_field(wp_unslash($_POST['http_status'] ?? '')),
                'error_text'     => sanitize_text_field(wp_unslash($_POST['error_text'] ?? '')),
                'payment_method' => sanitize_text_field(wp_unslash($_POST['payment_method'] ?? '')),
                'browser'        => sanitize_text_field($_SERVER['HTTP_USER_AGENT'] ?? ''),
            ),
        ));
        wp_send_json_success(array('message' => 'ok'));
    }

    public function log_login($user_login, $user) { $this->simple_user_log($user->ID, '账户', '登录', '用户登录成功', '登录流程', '用户成功登录网站。'); }
    public function log_logout() { $uid = get_current_user_id(); $this->simple_user_log($uid, '账户', '退出', '用户退出登录', '退出流程', '用户退出了当前登录状态。'); }
    public function log_register($user_id) { $this->simple_user_log($user_id, '账户', '注册', '用户注册新账户', '注册流程', '站点产生了一个新的注册账户。'); }
    public function log_profile_update($user_id, $old_user_data) { $this->simple_user_log($user_id, '账户', '资料更新', '用户更新账户资料', '我的账户 / 资料页', '用户更新了账户资料或相关字段。'); }
    private function simple_user_log($user_id, $module, $type, $name, $location, $details) {
        CUOL_Logger::log(compact('user_id') + array('module'=>$module,'action_type'=>$type,'action_name'=>$name,'severity'=>'info','location'=>$location,'object_type'=>'user','object_id'=>(int)$user_id,'details'=>$details));
    }

    public function log_comment_post($comment_id, $comment_approved, $commentdata) {
        CUOL_Logger::log(array('user_id'=>(int)($commentdata['user_id'] ?? 0),'module'=>'互动','action_type'=>'评论','action_name'=>'用户提交评论','severity'=>$comment_approved ? 'info' : 'warning','location'=>get_permalink((int)($commentdata['comment_post_ID'] ?? 0)),'object_type'=>'comment','object_id'=>$comment_id,'details'=>$comment_approved ? '用户提交了一条评论并通过审核。' : '用户提交了一条评论,但当前未审核通过。'));
    }

    public function log_add_to_cart($cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data) {
        if (!function_exists('wc_get_product')) return;
        $product = wc_get_product($variation_id ?: $product_id);
        CUOL_Logger::log(array('module'=>'商城','action_type'=>'购物车','action_name'=>'加入购物车','severity'=>'info','location'=>CUOL_Logger::detect_location(),'object_type'=>'product','object_id'=>$variation_id ?: $product_id,'details'=>sprintf('加入购物车:%s,数量:%d。', $product ? $product->get_name() : '未知商品', (int)$quantity),'extra_data'=>array('cart_item_key'=>$cart_item_key,'product_id'=>$product_id,'variation_id'=>$variation_id,'quantity'=>(int)$quantity)));
    }

    public function log_remove_cart_item($cart_item_key, $cart) {
        $removed = method_exists($cart, 'get_removed_cart_contents') ? $cart->get_removed_cart_contents() : array();
        $item = $removed[$cart_item_key] ?? null;
        CUOL_Logger::log(array('module'=>'商城','action_type'=>'购物车','action_name'=>'移出购物车','severity'=>'info','location'=>CUOL_Logger::detect_location(),'object_type'=>'product','object_id'=>(int)($item['product_id'] ?? 0),'details'=>'用户从购物车移除了商品。','extra_data'=>(array)$item));
    }

    public function log_applied_coupon($coupon_code) { CUOL_Logger::log(array('module'=>'商城','action_type'=>'优惠券','action_name'=>'使用优惠券','severity'=>'info','details'=>'用户使用优惠券:' . sanitize_text_field($coupon_code))); }
    public function log_removed_coupon($coupon_code) { CUOL_Logger::log(array('module'=>'商城','action_type'=>'优惠券','action_name'=>'移除优惠券','severity'=>'info','details'=>'用户移除了优惠券:' . sanitize_text_field($coupon_code))); }

    public function log_checkout_validation($data, $errors) {
        if ($errors && method_exists($errors, 'get_error_messages')) {
            $messages = array_filter((array) $errors->get_error_messages());
            if (!empty($messages)) {
                CUOL_Logger::log(array('module'=>'结账/支付','action_type'=>'结账校验','action_name'=>'结账校验失败','severity'=>'warning','location'=>'结账页','details'=>implode(' | ', array_map('wp_strip_all_tags', $messages)),'extra_data'=>array('errors'=>$messages,'posted_fields'=>array_keys((array)$data))));
            }
        }
    }

    public function capture_checkout_notices($data, $errors) { }

    public function log_order_processed($order_id, $posted_data, $order) {
        if (!$order && function_exists('wc_get_order')) $order = wc_get_order($order_id);
        CUOL_Logger::log(array('user_id'=>$order ? $order->get_customer_id() : 0,'module'=>'结账/支付','action_type'=>'订单创建','action_name'=>'订单已创建,等待支付/处理中','severity'=>'info','location'=>'结账页','object_type'=>'order','object_id'=>$order_id,'details'=>'订单 #' . $order_id . ' 已创建。支付方式:' . ($order ? $order->get_payment_method_title() : ''),'extra_data'=>CUOL_Logger::build_order_context($order)));
    }

    public function log_payment_complete($order_id) {
        $order = function_exists('wc_get_order') ? wc_get_order($order_id) : false;
        CUOL_Logger::log(array('user_id'=>$order ? $order->get_customer_id() : 0,'module'=>'结账/支付','action_type'=>'支付结果','action_name'=>'支付成功','severity'=>'info','location'=>'支付回调 / 订单流程','object_type'=>'order','object_id'=>$order_id,'details'=>'订单 #' . $order_id . ' 支付成功。','extra_data'=>CUOL_Logger::build_order_context($order)));
    }

    public function log_order_status_changed($order_id, $from, $to, $order) {
        CUOL_Logger::log(array('user_id'=>$order ? $order->get_customer_id() : 0,'module'=>'订单','action_type'=>'状态变更','action_name'=>'订单状态发生变化','severity'=>'info','location'=>'订单流程','object_type'=>'order','object_id'=>$order_id,'details'=>'订单 #' . $order_id . ' 状态从 ' . $from . ' 变更为 ' . $to . '','extra_data'=>CUOL_Logger::build_order_context($order, array('from'=>$from,'to'=>$to))));
    }

    public function log_order_failed($order_id, $order) {
        if (!$order && function_exists('wc_get_order')) $order = wc_get_order($order_id);
        CUOL_Logger::log(array('user_id'=>$order ? $order->get_customer_id() : 0,'module'=>'结账/支付','action_type'=>'支付结果','action_name'=>'支付失败','severity'=>'error','location'=>'支付流程','object_type'=>'order','object_id'=>$order_id,'details'=>'订单 #' . $order_id . ' 支付失败。原因:' . (CUOL_Logger::extract_order_failure_reason($order) ?: '未提取到明确原因'),'extra_data'=>CUOL_Logger::build_order_context($order, array('failure_reason'=>CUOL_Logger::extract_order_failure_reason($order)))));
    }

    public function log_order_cancelled($order_id, $order) {
        if (!$order && function_exists('wc_get_order')) $order = wc_get_order($order_id);
        CUOL_Logger::log(array('user_id'=>$order ? $order->get_customer_id() : 0,'module'=>'结账/支付','action_type'=>'支付结果','action_name'=>'订单已取消','severity'=>'warning','location'=>'支付流程','object_type'=>'order','object_id'=>$order_id,'details'=>'订单 #' . $order_id . ' 已取消。','extra_data'=>CUOL_Logger::build_order_context($order)));
    }

    public function log_thankyou_result($order_id) {
        if (!$order_id) return;
        $order = function_exists('wc_get_order') ? wc_get_order($order_id) : false;
        CUOL_Logger::log(array('user_id'=>$order ? $order->get_customer_id() : 0,'module'=>'结账/支付','action_type'=>'感谢页','action_name'=>'到达感谢页','severity'=>'info','location'=>'感谢页','object_type'=>'order','object_id'=>$order_id,'details'=>'用户已到达订单感谢页。','extra_data'=>CUOL_Logger::build_order_context($order)));
    }

    public function log_checkout_exception($order, $data) {
        CUOL_Logger::log(array('user_id'=>$order && is_a($order, 'WC_Order') ? $order->get_customer_id() : 0,'module'=>'结账/支付','action_type'=>'结账异常','action_name'=>'结账时抛出异常','severity'=>'error','location'=>'结账流程','object_type'=>$order ? 'order' : '','object_id'=>$order ? $order->get_id() : 0,'details'=>'WooCommerce 结账流程出现异常。','extra_data'=>CUOL_Logger::build_order_context($order, array('posted_fields'=>is_array($data) ? array_keys($data) : array()))));
    }
}
new CUOL_Plugin();

 

 

 class-cuol-admin.php文件

<?php
if (!defined('ABSPATH')) exit;

class CUOL_Admin {
    public static function init() {
        add_action('admin_menu', array(__CLASS__, 'register_menu'));
        add_action('admin_enqueue_scripts', array(__CLASS__, 'enqueue_assets'));
        add_action('admin_post_cuol_delete_single', array(__CLASS__, 'handle_delete_single'));
        add_action('admin_post_cuol_bulk_action', array(__CLASS__, 'handle_bulk_action'));
        add_action('admin_post_cuol_delete_all', array(__CLASS__, 'handle_delete_all'));
        add_action('admin_post_cuol_export_csv', array(__CLASS__, 'handle_export_csv'));
        add_action('admin_post_cuol_save_settings', array(__CLASS__, 'handle_save_settings'));
        add_action('admin_post_cuol_delete_visitor_data', array(__CLASS__, 'handle_delete_visitor_data'));
        add_action('admin_post_cuol_files_action', array(__CLASS__, 'handle_files_action'));
    }

    public static function register_menu() {
        add_menu_page('用户操作日志', '用户操作日志', 'manage_woocommerce', 'cuol-logs', array(__CLASS__, 'render_logs_page'), 'dashicons-list-view', 56);
        add_submenu_page('cuol-logs', '网站日志', '网站日志', 'manage_woocommerce', 'cuol-logs', array(__CLASS__, 'render_logs_page'));
        add_submenu_page('cuol-logs', '在线人数与访问统计', '在线人数与访问统计', 'manage_woocommerce', 'cuol-visitors', array(__CLASS__, 'render_visitors_page'));
        add_submenu_page('cuol-logs', '日志文件管理', '日志文件管理', 'manage_woocommerce', 'cuol-files', array(__CLASS__, 'render_files_page'));
    }

    public static function enqueue_assets($hook) {
        if (strpos((string) $hook, 'cuol-') === false) return;
        wp_enqueue_style('cuol-admin-style', CUOL_URL . 'assets/css/admin.css', array(), CUOL_VERSION);
        wp_enqueue_script('cuol-admin-js', CUOL_URL . 'assets/js/admin.js', array(), CUOL_VERSION, true);
        wp_localize_script('cuol-admin-js', 'CUOL_Admin', array(
            'ajax_url' => admin_url('admin-ajax.php'),
            'nonce' => wp_create_nonce('cuol_runtime_nonce'),
        ));
    }

    public static function handle_delete_single() {
        self::guard();
        check_admin_referer('cuol_delete_single');
        $id = absint($_GET['id'] ?? 0);
        if ($id) CUOL_Logger::delete_logs(array($id));
        wp_safe_redirect(self::back_to_logs(array('deleted' => 1)));
        exit;
    }

    public static function handle_bulk_action() {
        self::guard();
        check_admin_referer('cuol_bulk_action');
        $action = sanitize_text_field(wp_unslash($_POST['bulk_action'] ?? ''));
        $ids = array_map('absint', (array)($_POST['log_ids'] ?? array()));
        if ($action === 'delete' && $ids) CUOL_Logger::delete_logs($ids);
        wp_safe_redirect(self::back_to_logs(array('bulk_deleted' => 1)));
        exit;
    }

    public static function handle_delete_all() {
        self::guard();
        check_admin_referer('cuol_delete_all');
        CUOL_Logger::delete_all_logs();
        CUOL_File_Store::delete_all_files('ops');
        wp_safe_redirect(admin_url('admin.php?page=cuol-logs&all_deleted=1'));
        exit;
    }

    public static function handle_export_csv() {
        self::guard();
        check_admin_referer('cuol_export_csv');
        $filters = self::get_filter_values();
        $result = CUOL_Logger::get_logs(array_merge($filters, array('paged' => 1, 'per_page' => 5000)));
        nocache_headers();
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename=cuol-logs-' . gmdate('Ymd-His') . '.csv');
        $output = fopen('php://output', 'w');
        fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF));
        fputcsv($output, array('ID', '时间', '用户ID', '用户名', '角色', 'IP', '模块', '行为类型', '行为名称', '级别', '位置', '对象类型', '对象ID', '详情', '附加信息'));
        foreach ((array) $result['items'] as $item) {
            fputcsv($output, array(
                $item->id, $item->created_at, $item->user_id, $item->username, $item->user_role,
                $item->ip_address, $item->module, $item->action_type, $item->action_name, $item->severity,
                wp_strip_all_tags($item->location), $item->object_type, $item->object_id,
                wp_strip_all_tags($item->details), self::prettify_json($item->extra_data)
            ));
        }
        fclose($output);
        exit;
    }

    public static function handle_save_settings() {
        self::guard();
        check_admin_referer('cuol_save_settings');
        update_option(CUOL_OPTION_RETENTION_DAYS, absint($_POST['retention_days'] ?? 30));
        $settings = CUOL_Logger::default_tracking_settings();
        $posted = (array) ($_POST['tracking'] ?? array());
        foreach ($settings as $key => $default) {
            $settings[$key] = empty($posted[$key]) ? 0 : 1;
        }
        update_option(CUOL_OPTION_TRACKING_SETTINGS, $settings);
        wp_safe_redirect(wp_get_referer() ?: admin_url('admin.php?page=cuol-logs&settings_saved=1'));
        exit;
    }

    public static function handle_delete_visitor_data() {
        self::guard();
        check_admin_referer('cuol_delete_visitor_data');
        CUOL_Visitor::delete_all_records();
        CUOL_File_Store::delete_all_files('visitors');
        wp_safe_redirect(admin_url('admin.php?page=cuol-visitors&deleted=1'));
        exit;
    }

    public static function handle_files_action() {
        self::guard();
        check_admin_referer('cuol_files_action');
        $type = sanitize_text_field(wp_unslash($_POST['file_type'] ?? 'ops'));
        $type = in_array($type, array('ops', 'visitors'), true) ? $type : 'ops';
        $action = sanitize_text_field(wp_unslash($_POST['files_action'] ?? ''));
        $files = array_map('sanitize_file_name', (array)($_POST['file_names'] ?? array()));
        $dates = CUOL_File_Store::dates_from_names($files);
        if ($action === 'delete_selected') {
            CUOL_File_Store::delete_files($type, $files);
            if ($type === 'ops') CUOL_Logger::delete_logs_by_dates($dates);
            else CUOL_Visitor::delete_records_by_dates($dates);
        }
        if ($action === 'delete_all') {
            CUOL_File_Store::delete_all_files($type);
            if ($type === 'ops') CUOL_Logger::delete_all_logs();
            else CUOL_Visitor::delete_all_records();
        }
        wp_safe_redirect(admin_url('admin.php?page=cuol-files&type=' . rawurlencode($type) . '&done=1'));
        exit;
    }

    public static function render_logs_page() {
        self::guard();
        $filters = self::get_filter_values();
        $paged = max(1, absint($_GET['paged'] ?? 1));
        $per_page_options = array(10, 20, 50, 100, 500);
        $per_page = absint($_GET['per_page'] ?? 20);
        if (!in_array($per_page, $per_page_options, true)) $per_page = 20;
        $result = CUOL_Logger::get_logs(array_merge($filters, array('paged' => $paged, 'per_page' => $per_page)));
        $stats = CUOL_Logger::get_stats($filters);
        $items = (array) $result['items'];
        $total = (int) $result['total'];
        $total_pages = max(1, (int) ceil($total / max(1, $per_page)));
        $modules = (array) CUOL_Logger::get_module_options();
        $tracking = CUOL_Logger::get_tracking_settings();
        $retention_days = (int) get_option(CUOL_OPTION_RETENTION_DAYS, 30);
        ?>
        <div class="wrap cuol-wrap">
            <div class="cuol-header-card">
                <div>
                    <h1>网站日志记录</h1>
                    <p>集中查看全站访问、WooCommerce 购物车 / 结账 / 支付过程、失败原因与订单状态变化。</p>
                </div>
                <div class="cuol-top-actions">
                    <a class="button" href="<?php echo esc_url(admin_url('admin.php?page=cuol-visitors')); ?>">查看在线人数与访问统计</a>
                    <a class="button" href="<?php echo esc_url(admin_url('admin.php?page=cuol-files')); ?>">管理日志文件</a>
                    <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"><?php wp_nonce_field('cuol_export_csv'); foreach (array_merge($filters, array('per_page' => $per_page)) as $k => $v): ?><input type="hidden" name="<?php echo esc_attr($k); ?>" value="<?php echo esc_attr((string) $v); ?>"><?php endforeach; ?><input type="hidden" name="action" value="cuol_export_csv"><button type="submit" class="button button-primary">导出当前筛选 CSV</button></form>
                    <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" onsubmit="return confirm('确定要一键清空全部网站日志吗?此操作不可恢复。');"><?php wp_nonce_field('cuol_delete_all'); ?><input type="hidden" name="action" value="cuol_delete_all"><button type="submit" class="button cuol-danger-btn">一键清空全部日志</button></form>
                </div>
            </div>
            <?php self::notice('deleted', '日志已删除。'); self::notice('bulk_deleted', '已批量删除所选日志。'); self::notice('all_deleted', '全部日志已清空。'); self::notice('settings_saved', '设置已保存。'); ?>
            <div class="cuol-stats-grid cuol-stats-grid-5">
                <?php echo self::stat_link_box('日志总数', $stats['total'], self::build_filter_url(array('module' => '', 'severity' => '', 'order_id' => '', 'username' => '', 'paged' => 1))); ?>
                <?php echo self::stat_link_box('错误日志', $stats['errors'], self::build_filter_url(array('severity' => 'error', 'paged' => 1))); ?>
                <?php echo self::stat_link_box('结账/支付日志', $stats['checkout'], self::build_filter_url(array('module' => '结账/支付', 'paged' => 1))); ?>
                <?php echo self::stat_link_box('涉及订单数', $stats['orders'], self::build_filter_url(array('module' => '订单', 'paged' => 1))); ?>
                <?php echo self::stat_link_box('涉及登录用户', $stats['users'], self::build_filter_url(array('username' => '', 'paged' => 1))); ?>
            </div>

            <div class="cuol-filter-card">
                <form method="get" class="cuol-filter-form cuol-filter-form-6">
                    <input type="hidden" name="page" value="cuol-logs">
                    <input type="hidden" name="per_page" value="<?php echo esc_attr((string) $per_page); ?>">
                    <div class="cuol-field-group"><label>搜索关键词</label><input type="text" name="search" value="<?php echo esc_attr($filters['search']); ?>" placeholder="用户名、行为、详情、位置、附加信息"></div>
                    <div class="cuol-field-group"><label>模块</label><select name="module"><option value="">全部模块</option><?php foreach ($modules as $mod): ?><option value="<?php echo esc_attr($mod); ?>" <?php selected($filters['module'], $mod); ?>><?php echo esc_html($mod); ?></option><?php endforeach; ?></select></div>
                    <div class="cuol-field-group"><label>级别</label><select name="severity"><option value="">全部级别</option><option value="info" <?php selected($filters['severity'], 'info'); ?>>普通</option><option value="warning" <?php selected($filters['severity'], 'warning'); ?>>警告</option><option value="error" <?php selected($filters['severity'], 'error'); ?>>错误</option></select></div>
                    <div class="cuol-field-group"><label>订单号</label><input type="number" name="order_id" value="<?php echo esc_attr((string) $filters['order_id']); ?>"></div>
                    <div class="cuol-field-group"><label>用户名</label><input type="text" name="username" value="<?php echo esc_attr($filters['username']); ?>" placeholder="输入或选择用户名"></div>
                    <div class="cuol-field-group"><label>开始日期</label><input type="date" name="date_from" value="<?php echo esc_attr($filters['date_from']); ?>"></div>
                    <div class="cuol-field-group"><label>结束日期</label><input type="date" name="date_to" value="<?php echo esc_attr($filters['date_to']); ?>"></div>
                    <div class="cuol-filter-actions"><button class="button button-primary" type="submit">筛选日志</button><a class="button" href="<?php echo esc_url(admin_url('admin.php?page=cuol-logs')); ?>">重置筛选</a></div>
                </form>
            </div>

            <div class="cuol-filter-card">
                <h2 class="cuol-section-title">抓取内容设置</h2>
                <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="cuol-settings-form-block">
                    <?php wp_nonce_field('cuol_save_settings'); ?>
                    <input type="hidden" name="action" value="cuol_save_settings">
                    <div class="cuol-settings-grid">
                        <div>
                            <p class="cuol-muted">可以设置网站只抓取哪些内容。关闭后,新日志将不再记录对应类型。</p>
                            <div class="cuol-checkbox-grid">
                                <?php
                                $labels = array(
                                    'page_visit'   => '网站页面访问',
                                    'woo_cart'     => '商城访问 / 加购 / 优惠券',
                                    'woo_checkout' => '结账 / 支付错误',
                                    'woo_orders'   => '订单状态变化',
                                    'auth'         => '登录 / 注册 / 退出',
                                    'profile'      => '个人资料修改',
                                    'comments'     => '评论发布',
                                    'visitors'     => '在线人数与访问统计',
                                );
                                foreach ($labels as $key => $label): ?>
                                    <label class="cuol-checkbox-item"><input type="checkbox" name="tracking[<?php echo esc_attr($key); ?>]" value="1" <?php checked(!empty($tracking[$key])); ?>> <span><?php echo esc_html($label); ?></span></label>
                                <?php endforeach; ?>
                            </div>
                        </div>
                        <div class="cuol-settings-side">
                            <div class="cuol-field-inline"><label>日志保留天数</label><input type="number" name="retention_days" min="0" value="<?php echo esc_attr((string) $retention_days); ?>"></div>
                            <p class="cuol-muted">系统每天会自动清理更早的旧日志。填 0 表示不自动清理。</p>
                            <button type="submit" class="button button-primary">保存设置</button>
                        </div>
                    </div>
                </form>
            </div>

            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                <?php wp_nonce_field('cuol_bulk_action'); ?>
                <input type="hidden" name="action" value="cuol_bulk_action">
                <?php foreach (array_merge($filters, array('paged' => $paged, 'per_page' => $per_page)) as $k => $v): ?><input type="hidden" name="<?php echo esc_attr($k); ?>" value="<?php echo esc_attr((string) $v); ?>"><?php endforeach; ?>
                <div class="cuol-table-card">
                    <div class="cuol-bulk-bar">
                        <select name="bulk_action"><option value="">批量操作</option><option value="delete">删除所选日志</option></select>
                        <button type="submit" class="button">应用</button>
                        <div class="cuol-subtext">当前共 <?php echo esc_html(number_format_i18n($total)); ?> 条日志</div>
                    </div>
                    <table class="widefat striped cuol-table">
                        <thead>
                            <tr>
                                <td class="check-column"><input type="checkbox" id="cuol-check-all"></td>
                                <th>时间</th>
                                <th>用户</th>
                                <th>模块</th>
                                <th>行为</th>
                                <th>级别</th>
                                <th>位置</th>
                                <th>详情</th>
                                <th>操作</th>
                            </tr>
                        </thead>
                        <tbody>
                        <?php if (!$items): ?>
                            <tr><td colspan="9" class="cuol-empty">暂无日志</td></tr>
                        <?php else: foreach ($items as $item): ?>
                            <tr>
                                <th class="check-column"><input type="checkbox" name="log_ids[]" value="<?php echo esc_attr($item->id); ?>"></th>
                                <td><?php echo esc_html($item->created_at); ?><?php if ($item->object_id): ?><div class="cuol-subtext"><?php echo esc_html($item->object_type . ' #' . $item->object_id); ?></div><?php endif; ?></td>
                                <td><?php echo esc_html($item->username ?: '游客'); ?><div class="cuol-subtext">UID: <?php echo esc_html($item->user_id ?: 0); ?> / 角色: <?php echo esc_html($item->user_role ?: 'guest'); ?></div><div class="cuol-subtext">IP: <?php echo esc_html($item->ip_address); ?></div></td>
                                <td><span class="cuol-badge module"><?php echo esc_html($item->module); ?></span></td>
                                <td><?php echo esc_html($item->action_name); ?><div class="cuol-subtext"><?php echo esc_html($item->action_type); ?></div></td>
                                <td><?php echo wp_kses_post(self::severity_badge($item->severity)); ?></td>
                                <td class="cuol-location"><?php echo wp_kses_post(CUOL_Logger::display_location_html($item->location, $item->extra_data)); ?></td>
                                <td class="cuol-details"><?php echo esc_html(wp_strip_all_tags($item->details)); ?><?php if (!empty($item->extra_data) && $item->extra_data !== '[]' && $item->extra_data !== '{}'): ?><details class="cuol-extra-details"><summary>查看附加信息</summary><pre><?php echo esc_html(self::prettify_json($item->extra_data)); ?></pre></details><?php endif; ?></td>
                                <td><a class="button button-small" href="<?php echo esc_url(wp_nonce_url(admin_url('admin-post.php?action=cuol_delete_single&id=' . $item->id . '&paged=' . $paged . '&per_page=' . $per_page), 'cuol_delete_single')); ?>" onclick="return confirm('确定删除这条日志吗?');">删除</a></td>
                            </tr>
                        <?php endforeach; endif; ?>
                        </tbody>
                    </table>

                    <div class="cuol-table-footer">
                        <form method="get" class="cuol-per-page-form">
                            <input type="hidden" name="page" value="cuol-logs">
                            <?php foreach ($filters as $k => $v): ?><input type="hidden" name="<?php echo esc_attr($k); ?>" value="<?php echo esc_attr((string) $v); ?>"><?php endforeach; ?>
                            <label for="cuol-per-page">每页显示</label>
                            <select id="cuol-per-page" name="per_page" onchange="this.form.submit()">
                                <?php foreach ($per_page_options as $opt): ?><option value="<?php echo esc_attr((string) $opt); ?>" <?php selected($per_page, $opt); ?>><?php echo esc_html((string) $opt); ?></option><?php endforeach; ?>
                            </select>
                            <span>条</span>
                        </form>
                        <div class="cuol-pagination-wrap">
                            <?php if ($total_pages > 1): ?><div class="tablenav-pages"><?php echo wp_kses_post(paginate_links(array('base' => add_query_arg(array('paged' => '%#%')), 'format' => '', 'prev_text' => '«', 'next_text' => '»', 'total' => $total_pages, 'current' => $paged))); ?></div><?php endif; ?>
                            <span class="cuol-subtext">第 <?php echo esc_html(number_format_i18n($paged)); ?> / <?php echo esc_html(number_format_i18n($total_pages)); ?> 页</span>
                        </div>
                    </div>
                </div>
            </form>
        </div>
        <?php
    }

    public static function render_visitors_page() {
        self::guard();
        $stats = CUOL_Visitor::get_dashboard_stats();
        $chart = CUOL_Visitor::get_chart_data(14);
        $countries = CUOL_Visitor::get_country_stats(10);
        $recent = CUOL_Visitor::get_recent_online(30);
        ?>
        <div class="wrap cuol-wrap">
            <div class="cuol-header-card">
                <div><h1>在线人数与访问统计</h1><p>实时显示在线人数、今日/昨日访问量、国家来源分布,以及最近 14 天访问趋势。</p></div>
                <div class="cuol-top-actions"><a class="button" href="<?php echo esc_url(admin_url('admin.php?page=cuol-logs')); ?>">返回网站日志</a><a class="button" href="<?php echo esc_url(admin_url('admin.php?page=cuol-files&type=visitors')); ?>">管理人数统计文件</a><form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" onsubmit="return confirm('确定要清空访问统计数据库记录吗?');"><?php wp_nonce_field('cuol_delete_visitor_data'); ?><input type="hidden" name="action" value="cuol_delete_visitor_data"><button type="submit" class="button cuol-danger-btn">一键清空人数统计记录</button></form></div>
            </div>
            <?php self::notice('deleted', '访问统计记录已清空。'); ?>
            <div class="cuol-stats-grid cuol-stats-grid-4">
                <div class="cuol-stat-box cuol-live-box"><strong id="cuol-live-online"><?php echo esc_html(number_format_i18n($stats['online'])); ?></strong><span>在线人数(实时)</span></div>
                <div class="cuol-stat-box"><strong id="cuol-live-today"><?php echo esc_html(number_format_i18n($stats['today'])); ?></strong><span>今日访问用户</span></div>
                <div class="cuol-stat-box"><strong id="cuol-live-yesterday"><?php echo esc_html(number_format_i18n($stats['yesterday'])); ?></strong><span>昨日访问用户</span></div>
                <div class="cuol-stat-box"><strong id="cuol-live-pageviews"><?php echo esc_html(number_format_i18n($stats['pageviews_today'])); ?></strong><span>今日访问次数</span></div>
            </div>
            <div class="cuol-chart-grid">
                <div class="cuol-table-card"><div class="cuol-panel-title">近 14 天访问趋势</div><canvas id="cuol-chart" height="320"></canvas><script type="application/json" id="cuol-chart-data"><?php echo wp_json_encode($chart, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?></script></div>
                <div class="cuol-table-card"><div class="cuol-panel-title">来源国家 / 地区</div><table class="widefat striped"><thead><tr><th>国家/地区</th><th>人数</th></tr></thead><tbody><?php if (!$countries): ?><tr><td colspan="2">暂无数据</td></tr><?php else: foreach ($countries as $row): ?><tr><td><?php echo esc_html($row->country_name ?: '未知'); ?></td><td><?php echo esc_html(number_format_i18n($row->total)); ?></td></tr><?php endforeach; endif; ?></tbody></table></div>
            </div>
            <div class="cuol-table-card"><div class="cuol-panel-title">当前在线用户明细(实时刷新)</div><table class="widefat striped cuol-table"><thead><tr><th>最后活跃时间</th><th>用户</th><th>国家/地区</th><th>来源页</th><th>当前页</th><th>IP</th></tr></thead><tbody><?php if (!$recent): ?><tr><td colspan="6">暂无在线用户</td></tr><?php else: foreach ($recent as $item): ?><tr><td><?php echo esc_html($item->last_seen); ?></td><td><?php echo esc_html($item->username ?: '游客'); ?><div class="cuol-subtext"><?php echo esc_html($item->user_role ?: 'guest'); ?></div></td><td><?php echo esc_html($item->country_name ?: '未知'); ?></td><td class="cuol-location"><?php echo wp_kses_post(self::linked_url_html($item->landing_url)); ?></td><td class="cuol-location"><?php echo wp_kses_post(self::linked_url_html($item->current_url)); ?></td><td><?php echo esc_html($item->ip_address); ?></td></tr><?php endforeach; endif; ?></tbody></table></div>
        </div>
        <?php
    }

    public static function render_files_page() {
        self::guard();
        $type = sanitize_text_field(wp_unslash($_GET['type'] ?? 'ops'));
        if (!in_array($type, array('ops', 'visitors'), true)) $type = 'ops';
        $files = CUOL_File_Store::list_files($type);
        ?>
        <div class="wrap cuol-wrap">
            <div class="cuol-header-card"><div><h1>日志文件管理</h1><p>网站日志与人数统计日志分开存放。这里可以批量删除、单独删除或一键删除全部文件。</p></div><div class="cuol-top-actions"><a class="button <?php echo $type === 'ops' ? 'button-primary' : ''; ?>" href="<?php echo esc_url(admin_url('admin.php?page=cuol-files&type=ops')); ?>">网站日志文件</a><a class="button <?php echo $type === 'visitors' ? 'button-primary' : ''; ?>" href="<?php echo esc_url(admin_url('admin.php?page=cuol-files&type=visitors')); ?>">人数统计文件</a></div></div>
            <?php self::notice('done', '文件操作已完成,对应数据库记录也已同步处理。'); ?>
            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"><?php wp_nonce_field('cuol_files_action'); ?><input type="hidden" name="action" value="cuol_files_action"><input type="hidden" name="file_type" value="<?php echo esc_attr($type); ?>"><div class="cuol-table-card"><div class="cuol-bulk-bar"><select name="files_action"><option value="">批量操作</option><option value="delete_selected">删除所选文件</option><option value="delete_all">一键删除全部文件</option></select><button type="submit" class="button">应用</button><div class="cuol-subtext">文件存放目录:<?php echo esc_html(CUOL_File_Store::base_dir() . $type . '/'); ?></div></div><table class="widefat striped"><thead><tr><td class="check-column"><input type="checkbox" id="cuol-check-all-files"></td><th>文件名</th><th>大小</th><th>修改时间</th></tr></thead><tbody><?php if (!$files): ?><tr><td colspan="4">暂无文件</td></tr><?php else: foreach ($files as $file): ?><tr><th class="check-column"><input type="checkbox" name="file_names[]" value="<?php echo esc_attr($file['name']); ?>"></th><td><?php echo esc_html($file['name']); ?></td><td><?php echo esc_html(CUOL_File_Store::human_size($file['size'])); ?></td><td><?php echo esc_html(date_i18n('Y-m-d H:i:s', $file['mtime'])); ?></td></tr><?php endforeach; endif; ?></tbody></table></div></form>
        </div>
        <?php
    }

    private static function guard() {
        if (!current_user_can('manage_woocommerce')) wp_die('无权限执行此操作');
    }

    private static function get_filter_values() {
        return array(
            'search' => sanitize_text_field(wp_unslash($_REQUEST['search'] ?? '')),
            'module' => sanitize_text_field(wp_unslash($_REQUEST['module'] ?? '')),
            'severity' => sanitize_text_field(wp_unslash($_REQUEST['severity'] ?? '')),
            'order_id' => absint($_REQUEST['order_id'] ?? 0),
            'username' => sanitize_text_field(wp_unslash($_REQUEST['username'] ?? '')),
            'date_from' => sanitize_text_field(wp_unslash($_REQUEST['date_from'] ?? '')),
            'date_to' => sanitize_text_field(wp_unslash($_REQUEST['date_to'] ?? '')),
        );
    }

    private static function back_to_logs($extra = array()) {
        $args = array_merge(array('page' => 'cuol-logs'), self::get_filter_values(), array('paged' => absint($_REQUEST['paged'] ?? 1), 'per_page' => absint($_REQUEST['per_page'] ?? 20)), $extra);
        return add_query_arg($args, admin_url('admin.php'));
    }

    private static function notice($key, $text) {
        if (isset($_GET[$key])) echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($text) . '</p></div>';
    }

    private static function severity_badge($severity) {
        $map = array(
            'info' => '<span class="cuol-badge info">普通</span>',
            'warning' => '<span class="cuol-badge warning">警告</span>',
            'error' => '<span class="cuol-badge error">错误</span>',
        );
        return $map[$severity] ?? $map['info'];
    }

    private static function prettify_json($json) {
        $d = json_decode($json, true);
        return (json_last_error() === JSON_ERROR_NONE && is_array($d)) ? wp_json_encode($d, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : (string) $json;
    }

    private static function build_filter_url($args) {
        return esc_url(add_query_arg(array_merge(array('page' => 'cuol-logs', 'per_page' => absint($_GET['per_page'] ?? 20)), self::get_filter_values(), $args), admin_url('admin.php')));
    }

    private static function stat_link_box($label, $value, $url) {
        return '<a class="cuol-stat-box cuol-stat-link" href="' . esc_url($url) . '"><strong>' . esc_html(number_format_i18n($value)) . '</strong><span>' . esc_html($label) . '</span></a>';
    }

    private static function linked_url_html($url) {
        $url = CUOL_Logger::clean_display_url($url);
        if (!$url) return '<span class="cuol-subtext">无</span>';
        $text = preg_replace('#^https?://#', '', $url);
        return '<a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($text) . '</a>';
    }
}

 

class-cuol-file-store.php文件

<?php
if (!defined('ABSPATH')) exit;

class CUOL_File_Store {
    public static function base_dir() {
        $upload = wp_upload_dir();
        return trailingslashit($upload['basedir']) . 'cuol-data/';
    }
    public static function base_url() {
        $upload = wp_upload_dir();
        return trailingslashit($upload['baseurl']) . 'cuol-data/';
    }
    public static function ensure_directories() {
        wp_mkdir_p(self::base_dir() . 'ops');
        wp_mkdir_p(self::base_dir() . 'visitors');
        self::protect(self::base_dir());
        self::protect(self::base_dir() . 'ops/');
        self::protect(self::base_dir() . 'visitors/');
    }
    private static function protect($dir) {
        if (!file_exists($dir . 'index.php')) @file_put_contents($dir . 'index.php', "<?php\n// silence\n");
        if (!file_exists($dir . '.htaccess')) @file_put_contents($dir . '.htaccess', "Options -Indexes\n<FilesMatch \"\\.(jsonl|json|csv|log)$\">\nDeny from all\n</FilesMatch>\n");
    }
    public static function append($type, $row) {
        self::ensure_directories();
        $type = $type === 'visitors' ? 'visitors' : 'ops';
        $file = self::base_dir() . $type . '/' . gmdate('Y-m-d') . '.jsonl';
        $line = wp_json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
        @file_put_contents($file, $line, FILE_APPEND | LOCK_EX);
    }
    public static function list_files($type) {
        $type = $type === 'visitors' ? 'visitors' : 'ops';
        $dir = self::base_dir() . $type . '/';
        self::ensure_directories();
        $items = array();
        foreach (glob($dir . '*.*') ?: array() as $file) {
            if (is_dir($file)) continue;
            $base = basename($file);
            if (in_array($base, array('index.php', '.htaccess'), true)) continue;
            $items[] = array(
                'name' => $base,
                'path' => $file,
                'size' => filesize($file),
                'mtime' => filemtime($file),
                'date' => self::date_from_filename($base),
            );
        }
        usort($items, function($a,$b){ return $b['mtime'] <=> $a['mtime']; });
        return $items;
    }
    public static function date_from_filename($name) {
        $safe = sanitize_file_name($name);
        if (preg_match('/^(\d{4}-\d{2}-\d{2})\.(jsonl|json|csv|log)$/', $safe, $m)) {
            return $m[1];
        }
        return '';
    }
    public static function dates_from_names($names = array()) {
        $dates = array();
        foreach ((array) $names as $name) {
            $date = self::date_from_filename($name);
            if ($date) $dates[] = $date;
        }
        return array_values(array_unique($dates));
    }
    public static function delete_files($type, $names = array()) {
        $type = $type === 'visitors' ? 'visitors' : 'ops';
        $dir = self::base_dir() . $type . '/';
        $count = 0;
        foreach ((array)$names as $name) {
            $safe = sanitize_file_name($name);
            $path = $dir . $safe;
            if ($safe && file_exists($path) && is_file($path)) {
                @unlink($path);
                $count++;
            }
        }
        return $count;
    }
    public static function delete_all_files($type) {
        $files = self::list_files($type);
        return self::delete_files($type, wp_list_pluck($files, 'name'));
    }
    public static function cleanup_old_files() {
        $days = (int) get_option(CUOL_OPTION_RETENTION_DAYS, 30);
        if ($days <= 0) return;
        $threshold = time() - ($days * DAY_IN_SECONDS);
        foreach (array('ops','visitors') as $type) {
            foreach (self::list_files($type) as $file) {
                if ((int)$file['mtime'] < $threshold) @unlink($file['path']);
            }
        }
    }
    public static function human_size($size) {
        if ($size >= 1048576) return round($size/1048576, 2) . ' MB';
        if ($size >= 1024) return round($size/1024, 2) . ' KB';
        return intval($size) . ' B';
    }
}

 

class-cuol-logger.php文件

<?php
if (!defined('ABSPATH')) exit;

class CUOL_Logger {
    public static function init() {}

    public static function create_table() {
        global $wpdb;
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        $table = $wpdb->prefix . 'cuol_logs';
        $charset = $wpdb->get_charset_collate();
        $sql = "CREATE TABLE {$table} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            created_at DATETIME NOT NULL,
            user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            username VARCHAR(190) NOT NULL DEFAULT '',
            user_role VARCHAR(190) NOT NULL DEFAULT '',
            ip_address VARCHAR(100) NOT NULL DEFAULT '',
            module VARCHAR(190) NOT NULL DEFAULT '',
            action_type VARCHAR(190) NOT NULL DEFAULT '',
            action_name VARCHAR(190) NOT NULL DEFAULT '',
            severity VARCHAR(20) NOT NULL DEFAULT 'info',
            location TEXT NULL,
            object_type VARCHAR(50) NOT NULL DEFAULT '',
            object_id BIGINT NOT NULL DEFAULT 0,
            details LONGTEXT NULL,
            extra_data LONGTEXT NULL,
            PRIMARY KEY (id),
            KEY created_at (created_at),
            KEY user_id (user_id),
            KEY severity (severity),
            KEY module (module),
            KEY object_id (object_id)
        ) {$charset};";
        dbDelta($sql);
    }

    public static function default_tracking_settings() {
        return array(
            'page_visit'    => 1,
            'woo_cart'      => 1,
            'woo_checkout'  => 1,
            'woo_orders'    => 1,
            'auth'          => 1,
            'profile'       => 1,
            'comments'      => 1,
            'visitors'      => 1,
        );
    }

    public static function get_tracking_settings() {
        $saved = get_option(CUOL_OPTION_TRACKING_SETTINGS, array());
        return wp_parse_args(is_array($saved) ? $saved : array(), self::default_tracking_settings());
    }

    public static function is_tracking_enabled($key) {
        $settings = self::get_tracking_settings();
        return !empty($settings[$key]);
    }

    public static function should_track_payload($data) {
        $module = (string) ($data['module'] ?? '');
        $action = (string) ($data['action_name'] ?? '');
        $type   = (string) ($data['action_type'] ?? '');

        if (in_array($module, array('网站访问', '文章', '页面', '内容归档', '商品分类'), true)) {
            return self::is_tracking_enabled('page_visit');
        }
        if (in_array($module, array('商城', '商品'), true) && in_array($type, array('购物车', '优惠券', '访问'), true)) {
            return self::is_tracking_enabled('woo_cart');
        }
        if ($module === '结账/支付') {
            return self::is_tracking_enabled('woo_checkout');
        }
        if ($module === '订单') {
            return self::is_tracking_enabled('woo_orders');
        }
        if (in_array($action, array('用户登录', '用户退出登录', '新用户注册'), true)) {
            return self::is_tracking_enabled('auth');
        }
        if ($action === '更新个人资料') {
            return self::is_tracking_enabled('profile');
        }
        if ($action === '发表评论') {
            return self::is_tracking_enabled('comments');
        }
        return true;
    }

    public static function should_ignore_request($url = '') {
        $url = $url ?: self::current_url();
        if (!$url) return false;
        if (stripos($url, 'favicon.ico') !== false) return true;
        if (stripos($url, '/wp-admin/admin-ajax.php') !== false) return true;
        if (stripos($url, 'preview=true') !== false) return true;
        $uri = wp_parse_url($url, PHP_URL_PATH);
        if (!$uri) $uri = $_SERVER['REQUEST_URI'] ?? '';
        $uri = strtolower((string) $uri);
        if ($uri === '' || $uri === '/') return false;
        if (preg_match('/\/(favicon\.ico|robots\.txt)$/', $uri)) return true;
        if (preg_match('/\.(?:css|js|map|ico|png|jpg|jpeg|gif|svg|webp|avif|woff|woff2|ttf|eot|txt|xml|json|pdf|zip)(\?.*)?$/', $uri)) return true;
        if (strpos($uri, '/wp-json/') !== false) return true;
        return false;
    }

    public static function normalize_checkout_action_name($action_name, $details = '') {
        $details = wp_strip_all_tags((string) $details);
        if (preg_match('/card number.*incomplete|你的?卡号.*不完整|card number.*invalid/i', $details)) {
            return '银行卡信息不完整';
        }
        if (preg_match('/network connection error|network error|please refresh and try again|网络.*错误|连接.*错误/i', $details)) {
            return '网络连接异常';
        }
        if (preg_match('/security code.*incomplete|cvv|cvc|安全码.*不完整/i', $details)) {
            return '安全码信息不完整';
        }
        if (preg_match('/expiration date.*incomplete|expiry|有效期.*不完整/i', $details)) {
            return '有效期信息不完整';
        }
        return (string) $action_name;
    }

    public static function log($data = array()) {
        global $wpdb;
        $table = $wpdb->prefix . 'cuol_logs';
        $defaults = array(
            'user_id'     => get_current_user_id(),
            'module'      => '',
            'action_type' => '',
            'action_name' => '',
            'severity'    => 'info',
            'location'    => self::detect_location(),
            'object_type' => '',
            'object_id'   => 0,
            'details'     => '',
            'extra_data'  => array(),
        );
        $data = wp_parse_args($data, $defaults);
        if ($data['module'] === '结账/支付') {
            $data['action_name'] = self::normalize_checkout_action_name($data['action_name'], $data['details']);
        }
        if (!self::should_track_payload($data)) return false;

        $user_id = (int) $data['user_id'];
        $user = $user_id ? get_userdata($user_id) : null;
        $username = $user ? $user->user_login : '游客';
        $role = $user && !empty($user->roles) ? implode(',', (array) $user->roles) : 'guest';

        if ($user && user_can($user, 'manage_options')) return false;

        $created_at = current_time('mysql');
        $inserted = $wpdb->insert(
            $table,
            array(
                'created_at'  => $created_at,
                'user_id'     => $user_id,
                'username'    => sanitize_text_field($username),
                'user_role'   => sanitize_text_field($role),
                'ip_address'  => sanitize_text_field(self::get_ip_address()),
                'module'      => sanitize_text_field($data['module']),
                'action_type' => sanitize_text_field($data['action_type']),
                'action_name' => sanitize_text_field($data['action_name']),
                'severity'    => sanitize_text_field($data['severity']),
                'location'    => wp_strip_all_tags((string) $data['location']),
                'object_type' => sanitize_text_field($data['object_type']),
                'object_id'   => (int) $data['object_id'],
                'details'     => wp_kses_post((string) $data['details']),
                'extra_data'  => wp_json_encode($data['extra_data'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            ),
            array('%s','%d','%s','%s','%s','%s','%s','%s','%s','%s','%s','%d','%s','%s')
        );

        if ($inserted) {
            CUOL_File_Store::append('ops', array(
                'time' => $created_at,
                'user_id' => $user_id,
                'username' => sanitize_text_field($username),
                'role' => sanitize_text_field($role),
                'ip' => sanitize_text_field(self::get_ip_address()),
                'module' => sanitize_text_field($data['module']),
                'action_type' => sanitize_text_field($data['action_type']),
                'action_name' => sanitize_text_field($data['action_name']),
                'severity' => sanitize_text_field($data['severity']),
                'location' => wp_strip_all_tags((string) $data['location']),
                'object_type' => sanitize_text_field($data['object_type']),
                'object_id' => (int) $data['object_id'],
                'details' => wp_strip_all_tags((string) $data['details']),
                'extra_data' => $data['extra_data'],
            ));
        }
        return (bool) $inserted;
    }

    public static function detect_location($url = '') {
        $url = $url ?: self::current_url();
        $label = '前台页面';
        if ($url && strpos($url, '/checkout') !== false) {
            $label = '结账页';
        } elseif ($url && strpos($url, '/cart') !== false) {
            $label = '购物车页';
        } elseif (function_exists('is_checkout') && is_checkout()) {
            $label = '结账页';
        } elseif (function_exists('is_cart') && is_cart()) {
            $label = '购物车页';
        } elseif (function_exists('is_shop') && is_shop()) {
            $label = '商店页';
        } elseif (function_exists('is_product') && is_product()) {
            $label = '商品详情页';
        } elseif (function_exists('is_account_page') && is_account_page()) {
            $label = '我的账户页';
        } elseif (is_front_page()) {
            $label = '首页';
        } elseif (is_admin()) {
            $label = '后台';
        }
        return trim($label . ($url ? ' | ' . $url : ''));
    }

    public static function split_location($location) {
        $parts = array_map('trim', explode('|', (string) $location, 2));
        return array('label' => $parts[0] ?? '', 'url' => isset($parts[1]) ? trim($parts[1]) : '');
    }

    public static function current_url() {
        $scheme = is_ssl() ? 'https://' : 'http://';
        $host = $_SERVER['HTTP_HOST'] ?? '';
        $uri = $_SERVER['REQUEST_URI'] ?? '';
        if (!$host && !$uri) return '';
        return esc_url_raw($scheme . $host . $uri);
    }

    public static function clean_display_url($url) {
        $url = esc_url_raw((string) $url);
        if (!$url) return '';
        if (stripos($url, 'favicon.ico') !== false) return '';
        return $url;
    }

    public static function display_location_html($location, $extra_json = '') {
        $split = self::split_location($location);
        $label = $split['label'];
        $url = self::clean_display_url($split['url']);
        if (!$url && $extra_json) {
            $extra = json_decode($extra_json, true);
            if (is_array($extra) && !empty($extra['url'])) $url = self::clean_display_url($extra['url']);
            if (!$url && is_array($extra) && !empty($extra['page_url'])) $url = self::clean_display_url($extra['page_url']);
            if (!$url && is_array($extra) && !empty($extra['request_url'])) $url = self::clean_display_url($extra['request_url']);
        }
        $label_html = esc_html($label ?: '位置未知');
        if (!$url) return $label_html;
        $text = preg_replace('#^https?://#', '', $url);
        return $label_html . ' | <a href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($text) . '</a>';
    }

    public static function get_ip_address() {
        $keys = array('HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR');
        foreach ($keys as $key) {
            if (!empty($_SERVER[$key])) {
                $raw = sanitize_text_field(wp_unslash($_SERVER[$key]));
                $parts = explode(',', $raw);
                $ip = trim((string) $parts[0]);
                return self::mask_ip($ip);
            }
        }
        return '';
    }

    public static function mask_ip($ip) {
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            $parts = explode('.', $ip);
            $parts[3] = '***';
            return implode('.', $parts);
        }
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            return preg_replace('/:[a-f0-9]{1,4}$/i', ':****', $ip);
        }
        return $ip;
    }

    public static function build_order_context($order = false, $extra = array()) {
        $context = array();
        if ($order && is_a($order, 'WC_Order')) {
            $context = array(
                'order_id'             => $order->get_id(),
                'order_number'         => $order->get_order_number(),
                'status'               => $order->get_status(),
                'currency'             => $order->get_currency(),
                'total'                => $order->get_total(),
                'payment_method'       => $order->get_payment_method(),
                'payment_method_title' => $order->get_payment_method_title(),
                'billing_email'        => $order->get_billing_email(),
                'billing_phone'        => $order->get_billing_phone(),
                'customer_id'          => $order->get_customer_id(),
                'customer_note'        => $order->get_customer_note(),
                'transaction_id'       => method_exists($order, 'get_transaction_id') ? $order->get_transaction_id() : '',
            );
        }
        return array_merge($context, $extra);
    }

    public static function extract_order_failure_reason($order = false) {
        if (!$order || !is_a($order, 'WC_Order')) return '';
        $reasons = array();
        if (method_exists($order, 'get_customer_note') && $order->get_customer_note()) $reasons[] = wp_strip_all_tags($order->get_customer_note());
        if (method_exists($order, 'get_meta')) {
            $meta_keys = array('_transaction_id', '_stripe_decline_reason', '_stripe_intent_status', '_payment_result', '_failure_reason');
            foreach ($meta_keys as $key) {
                $value = $order->get_meta($key, true);
                if (!empty($value) && is_scalar($value)) $reasons[] = $key . ': ' . wp_strip_all_tags((string) $value);
            }
        }
        if (function_exists('wc_get_order_notes')) {
            $notes = wc_get_order_notes(array('order_id' => $order->get_id(), 'type' => 'internal'));
            if (!empty($notes)) {
                foreach (array_slice($notes, 0, 5) as $note) {
                    if (!empty($note->content)) {
                        $content = wp_strip_all_tags($note->content);
                        if (preg_match('/失败|failed|declin|error|incorrect|invalid|timeout|network/i', $content)) $reasons[] = $content;
                    }
                }
            }
        }
        return implode(' | ', array_values(array_unique(array_filter($reasons))));
    }

    public static function get_logs($args = array()) {
        global $wpdb;
        $table = $wpdb->prefix . 'cuol_logs';
        $defaults = array('paged'=>1,'per_page'=>20,'search'=>'','module'=>'','severity'=>'','order_id'=>0,'username'=>'','date_from'=>'','date_to'=>'');
        $args = wp_parse_args($args, $defaults);
        $where = array('1=1');
        $params = array();
        if ($args['search'] !== '') {
            $like = '%' . $wpdb->esc_like($args['search']) . '%';
            $where[] = '(username LIKE %s OR action_name LIKE %s OR details LIKE %s OR location LIKE %s OR extra_data LIKE %s)';
            array_push($params, $like, $like, $like, $like, $like);
        }
        if ($args['module'] !== '') { $where[] = 'module = %s'; $params[] = $args['module']; }
        if ($args['severity'] !== '') { $where[] = 'severity = %s'; $params[] = $args['severity']; }
        if (!empty($args['order_id'])) { $where[] = 'object_id = %d'; $params[] = (int) $args['order_id']; }
        if ($args['username'] !== '') { $where[] = 'username = %s'; $params[] = $args['username']; }
        if ($args['date_from'] !== '') { $where[] = 'created_at >= %s'; $params[] = sanitize_text_field($args['date_from']) . ' 00:00:00'; }
        if ($args['date_to'] !== '') { $where[] = 'created_at <= %s'; $params[] = sanitize_text_field($args['date_to']) . ' 23:59:59'; }

        $where_sql = implode(' AND ', $where);
        $offset = max(0, ((int) $args['paged'] - 1) * (int) $args['per_page']);
        $count_sql = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}";
        $data_sql = "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY id DESC LIMIT %d OFFSET %d";
        $total = (int) $wpdb->get_var($wpdb->prepare($count_sql, $params));
        $params_with_limit = $params;
        $params_with_limit[] = (int) $args['per_page'];
        $params_with_limit[] = (int) $offset;
        $items = $wpdb->get_results($wpdb->prepare($data_sql, $params_with_limit));
        return array('total' => $total, 'items' => $items);
    }

    public static function get_stats($filters = array()) {
        global $wpdb;
        $table = $wpdb->prefix . 'cuol_logs';
        $where = array('1=1');
        $params = array();
        if (!empty($filters['date_from'])) { $where[] = 'created_at >= %s'; $params[] = sanitize_text_field($filters['date_from']) . ' 00:00:00'; }
        if (!empty($filters['date_to'])) { $where[] = 'created_at <= %s'; $params[] = sanitize_text_field($filters['date_to']) . ' 23:59:59'; }
        $where_sql = implode(' AND ', $where);
        $base_sql = " FROM {$table} WHERE {$where_sql}";
        $total = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) {$base_sql}", $params));
        $errors = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) {$base_sql} AND severity = 'error'", $params));
        $checkout = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) {$base_sql} AND module = '结账/支付'", $params));
        $orders = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(DISTINCT object_id) {$base_sql} AND object_type = 'order' AND object_id > 0", $params));
        $users = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(DISTINCT user_id) {$base_sql} AND user_id > 0", $params));
        return compact('total','errors','checkout','orders','users');
    }

    public static function delete_logs($ids = array()) {
        global $wpdb;
        $table = $wpdb->prefix . 'cuol_logs';
        $ids = array_filter(array_map('absint', (array) $ids));
        if (empty($ids)) return false;
        $placeholders = implode(',', array_fill(0, count($ids), '%d'));
        return $wpdb->query($wpdb->prepare("DELETE FROM {$table} WHERE id IN ({$placeholders})", $ids));
    }

    public static function delete_logs_by_dates($dates = array()) {
        global $wpdb;
        $dates = array_values(array_filter(array_map('sanitize_text_field', (array) $dates)));
        if (!$dates) return 0;
        $table = $wpdb->prefix . 'cuol_logs';
        $placeholders = implode(',', array_fill(0, count($dates), '%s'));
        return (int) $wpdb->query($wpdb->prepare("DELETE FROM {$table} WHERE DATE(created_at) IN ({$placeholders})", $dates));
    }

    public static function delete_all_logs() {
        global $wpdb;
        return $wpdb->query("TRUNCATE TABLE {$wpdb->prefix}cuol_logs");
    }

    public static function cleanup_old_logs() {
        global $wpdb;
        $days = (int) get_option(CUOL_OPTION_RETENTION_DAYS, 30);
        if ($days <= 0) return 0;
        $threshold = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
        return $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}cuol_logs WHERE created_at < %s", $threshold));
    }

    public static function get_module_options() {
        global $wpdb;
        return $wpdb->get_col("SELECT DISTINCT module FROM {$wpdb->prefix}cuol_logs WHERE module != '' ORDER BY module ASC");
    }

    public static function get_username_options($limit = 100) {
        global $wpdb;
        return $wpdb->get_col($wpdb->prepare("SELECT DISTINCT username FROM {$wpdb->prefix}cuol_logs WHERE username != '' ORDER BY username ASC LIMIT %d", max(1, absint($limit))));
    }
}

 

class-cuol-visitor.php文件

<?php
if (!defined('ABSPATH')) exit;

class CUOL_Visitor {
    public static function init() {}

    public static function create_table() {
        global $wpdb;
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        $table = $wpdb->prefix . 'cuol_visitors';
        $charset = $wpdb->get_charset_collate();
        $sql = "CREATE TABLE {$table} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            session_key VARCHAR(100) NOT NULL,
            user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            username VARCHAR(190) NOT NULL DEFAULT '',
            user_role VARCHAR(190) NOT NULL DEFAULT '',
            is_admin TINYINT(1) NOT NULL DEFAULT 0,
            ip_address VARCHAR(100) NOT NULL DEFAULT '',
            country_code VARCHAR(10) NOT NULL DEFAULT '',
            country_name VARCHAR(100) NOT NULL DEFAULT '未知',
            referer TEXT NULL,
            landing_url TEXT NULL,
            current_url TEXT NULL,
            user_agent TEXT NULL,
            first_seen DATETIME NOT NULL,
            last_seen DATETIME NOT NULL,
            hit_count INT UNSIGNED NOT NULL DEFAULT 1,
            PRIMARY KEY (id),
            UNIQUE KEY session_key (session_key),
            KEY last_seen (last_seen),
            KEY first_seen (first_seen),
            KEY country_name (country_name),
            KEY is_admin (is_admin),
            KEY user_id (user_id)
        ) {$charset};";
        dbDelta($sql);
    }

    public static function get_country_data() {
        $country = array('code' => '', 'name' => '未知');
        if (class_exists('WC_Geolocation')) {
            $ip = self::raw_ip();
            if ($ip) {
                $geo = WC_Geolocation::geolocate_ip($ip);
                if (!empty($geo['country'])) {
                    $country['code'] = sanitize_text_field($geo['country']);
                    if (function_exists('WC') && WC()->countries) {
                        $countries = WC()->countries->get_countries();
                        $country['name'] = $countries[$geo['country']] ?? $geo['country'];
                    } else {
                        $country['name'] = $geo['country'];
                    }
                }
            }
        }
        return $country;
    }

    public static function session_key($override = '') {
        $uid = get_current_user_id();
        $override = sanitize_text_field((string) $override);
        if ($override && preg_match('/^[a-zA-Z0-9_-]{8,64}$/', $override)) {
            if (!headers_sent()) setcookie('cuol_vid', $override, time() + YEAR_IN_SECONDS, COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true);
            $_COOKIE['cuol_vid'] = $override;
            return $override;
        }
        $cookie = $_COOKIE['cuol_vid'] ?? '';
        if ($cookie) return sanitize_text_field(wp_unslash($cookie));
        $seed = ($uid ? 'u'.$uid : 'g') . '|' . wp_generate_uuid4();
        $key = substr(md5($seed), 0, 24);
        if (!headers_sent()) setcookie('cuol_vid', $key, time() + YEAR_IN_SECONDS, COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true);
        $_COOKIE['cuol_vid'] = $key;
        return $key;
    }

    public static function ajax_ping() {
        check_ajax_referer('cuol_runtime_nonce', 'nonce');
        $page_url = esc_url_raw(wp_unslash($_POST['page_url'] ?? ''));
        $visitor_key = sanitize_text_field(wp_unslash($_POST['visitor_key'] ?? ''));
        self::track_current_request(true, false, $page_url, $visitor_key);
        self::cleanup_stale_records();
        wp_send_json_success(array(
            'online' => self::get_online_count(false),
            'admin_online' => self::get_online_count(true),
            'today' => self::count_unique_by_day(current_time('Y-m-d')),
            'yesterday' => self::count_unique_by_day(date('Y-m-d', current_time('timestamp') - DAY_IN_SECONDS)),
            'pageviews_today' => self::pageviews_by_day(current_time('Y-m-d')),
        ));
    }

    public static function ajax_live_counts() {
        check_ajax_referer('cuol_runtime_nonce', 'nonce');
        self::cleanup_stale_records();
        wp_send_json_success(array(
            'online' => self::get_online_count(false),
            'admin_online' => self::get_online_count(true),
            'today' => self::count_unique_by_day(current_time('Y-m-d')),
            'yesterday' => self::count_unique_by_day(date('Y-m-d', current_time('timestamp') - DAY_IN_SECONDS)),
            'pageviews_today' => self::pageviews_by_day(current_time('Y-m-d')),
        ));
    }

    public static function track_current_request($heartbeat = false, $force_admin = false, $page_url = '', $visitor_key = '') {
        if (!CUOL_Logger::is_tracking_enabled('visitors')) return;
        $current_url = $page_url ? esc_url_raw($page_url) : CUOL_Logger::current_url();
        if (CUOL_Logger::should_ignore_request($current_url)) return;

        global $wpdb;
        $table = $wpdb->prefix . 'cuol_visitors';
        $uid = get_current_user_id();
        $user = $uid ? get_userdata($uid) : null;
        $is_admin = $force_admin ? 1 : 0;
        if ($user && user_can($user, 'manage_options')) $is_admin = 1;
        elseif (!$force_admin) $is_admin = is_admin() ? 1 : 0;

        $username = $user ? $user->user_login : '游客';
        $role = $user ? implode(',', (array) $user->roles) : 'guest';
        $session_key = self::session_key($visitor_key);
        $now = current_time('mysql');
        $country = self::get_country_data();
        $landing_url = $current_url;
        $referer = isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : '';
        $ip = CUOL_Logger::get_ip_address();
        $user_agent = sanitize_text_field($_SERVER['HTTP_USER_AGENT'] ?? '');

        $exists = $wpdb->get_row($wpdb->prepare("SELECT id, landing_url, hit_count FROM {$table} WHERE session_key = %s", $session_key));
        if ($exists) {
            $landing_url = !empty($exists->landing_url) ? $exists->landing_url : $landing_url;
            $wpdb->update($table, array(
                'user_id' => $uid,
                'username' => sanitize_text_field($username),
                'user_role' => sanitize_text_field($role),
                'is_admin' => (int) $is_admin,
                'ip_address' => $ip,
                'country_code' => sanitize_text_field($country['code']),
                'country_name' => sanitize_text_field($country['name']),
                'referer' => $referer,
                'landing_url' => $landing_url,
                'current_url' => $current_url,
                'user_agent' => $user_agent,
                'last_seen' => $now,
                'hit_count' => (int) $exists->hit_count + ($heartbeat ? 0 : 1),
            ), array('id' => (int) $exists->id));
        } else {
            $wpdb->insert($table, array(
                'session_key' => $session_key,
                'user_id' => $uid,
                'username' => sanitize_text_field($username),
                'user_role' => sanitize_text_field($role),
                'is_admin' => (int) $is_admin,
                'ip_address' => $ip,
                'country_code' => sanitize_text_field($country['code']),
                'country_name' => sanitize_text_field($country['name']),
                'referer' => $referer,
                'landing_url' => $landing_url,
                'current_url' => $current_url,
                'user_agent' => $user_agent,
                'first_seen' => $now,
                'last_seen' => $now,
                'hit_count' => 1,
            ));
            CUOL_File_Store::append('visitors', array(
                'time' => $now,
                'session_key' => $session_key,
                'user_id' => $uid,
                'username' => $username,
                'country' => $country['name'],
                'is_admin' => $is_admin,
                'landing_url' => $landing_url,
                'referer' => $referer,
                'ip' => $ip,
            ));
        }
    }

    public static function get_online_count($admin_only = false) {
        global $wpdb;
        $threshold = date('Y-m-d H:i:s', current_time('timestamp') - (CUOL_VISITOR_TIMEOUT_MINUTES * MINUTE_IN_SECONDS));
        $sql = "SELECT COUNT(*) FROM {$wpdb->prefix}cuol_visitors WHERE last_seen >= %s";
        $sql .= $admin_only ? ' AND is_admin = 1' : ' AND is_admin = 0';
        return (int) $wpdb->get_var($wpdb->prepare($sql, $threshold));
    }

    public static function count_unique_by_day($date) {
        global $wpdb;
        return (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(DISTINCT session_key) FROM {$wpdb->prefix}cuol_visitors WHERE DATE(first_seen) = %s AND is_admin = 0", $date));
    }

    public static function pageviews_by_day($date) {
        global $wpdb;
        return (int) $wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(hit_count),0) FROM {$wpdb->prefix}cuol_visitors WHERE DATE(first_seen) = %s AND is_admin = 0", $date));
    }

    public static function get_dashboard_stats() {
        $today = current_time('Y-m-d');
        $yesterday = date('Y-m-d', current_time('timestamp') - DAY_IN_SECONDS);
        return array(
            'online' => self::get_online_count(false),
            'today' => self::count_unique_by_day($today),
            'yesterday' => self::count_unique_by_day($yesterday),
            'pageviews_today' => self::pageviews_by_day($today),
        );
    }

    public static function get_chart_data($days = 14) {
        global $wpdb;
        $table = $wpdb->prefix . 'cuol_visitors';
        $days = max(2, absint($days));
        $out = array();
        for ($i = $days - 1; $i >= 0; $i--) {
            $date = date('Y-m-d', current_time('timestamp') - ($i * DAY_IN_SECONDS));
            $visitors = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE DATE(first_seen) = %s AND is_admin = 0", $date));
            $pageviews = (int) $wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(hit_count),0) FROM {$table} WHERE DATE(first_seen) = %s AND is_admin = 0", $date));
            $out[] = array('date' => $date, 'visitors' => $visitors, 'pageviews' => $pageviews);
        }
        return $out;
    }

    public static function get_country_stats($limit = 10) {
        global $wpdb;
        return $wpdb->get_results($wpdb->prepare("SELECT country_name, COUNT(*) AS total FROM {$wpdb->prefix}cuol_visitors WHERE is_admin = 0 GROUP BY country_name ORDER BY total DESC LIMIT %d", max(1, absint($limit))));
    }

    public static function get_recent_online($limit = 30) {
        global $wpdb;
        $threshold = date('Y-m-d H:i:s', current_time('timestamp') - (CUOL_VISITOR_TIMEOUT_MINUTES * MINUTE_IN_SECONDS));
        return $wpdb->get_results($wpdb->prepare("SELECT * FROM {$wpdb->prefix}cuol_visitors WHERE last_seen >= %s AND is_admin = 0 ORDER BY last_seen DESC LIMIT %d", $threshold, max(1, absint($limit))));
    }


    public static function cleanup_stale_records() {
        global $wpdb;
        $threshold = date('Y-m-d H:i:s', current_time('timestamp') - (max(10, CUOL_VISITOR_TIMEOUT_MINUTES * 12) * MINUTE_IN_SECONDS));
        $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}cuol_visitors WHERE last_seen < %s", $threshold));
    }

    public static function delete_all_records() {
        global $wpdb;
        return $wpdb->query("TRUNCATE TABLE {$wpdb->prefix}cuol_visitors");
    }

    public static function delete_records_by_dates($dates = array()) {
        global $wpdb;
        $dates = array_values(array_filter(array_map('sanitize_text_field', (array) $dates)));
        if (!$dates) return 0;
        $placeholders = implode(',', array_fill(0, count($dates), '%s'));
        return (int) $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}cuol_visitors WHERE DATE(first_seen) IN ({$placeholders})", $dates));
    }

    public static function cleanup_old_records() {
        global $wpdb;
        $days = (int) get_option(CUOL_OPTION_RETENTION_DAYS, 30);
        if ($days <= 0) return 0;
        $threshold = date('Y-m-d H:i:s', current_time('timestamp') - ($days * DAY_IN_SECONDS));
        return $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}cuol_visitors WHERE first_seen < %s", $threshold));
    }

    private static function raw_ip() {
        foreach (array('HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR') as $key) {
            if (!empty($_SERVER[$key])) {
                $raw = sanitize_text_field(wp_unslash($_SERVER[$key]));
                return trim(explode(',', $raw)[0]);
            }
        }
        return '';
    }
}

 

 admin.js文件

document.addEventListener('DOMContentLoaded', function(){
    var checkAll = document.getElementById('cuol-check-all');
    if (checkAll) {
        checkAll.addEventListener('change', function(){
            document.querySelectorAll('input[name="log_ids[]"]').forEach(function(el){ el.checked = checkAll.checked; });
        });
    }
    var checkAllFiles = document.getElementById('cuol-check-all-files');
    if (checkAllFiles) {
        checkAllFiles.addEventListener('change', function(){
            document.querySelectorAll('input[name="file_names[]"]').forEach(function(el){ el.checked = checkAllFiles.checked; });
        });
    }

    function drawChart(){
        var canvas = document.getElementById('cuol-chart');
        var jsonNode = document.getElementById('cuol-chart-data');
        if (!canvas || !jsonNode) return;
        try {
            var data = JSON.parse(jsonNode.textContent || '[]');
            if (!Array.isArray(data)) return;
            var ratio = window.devicePixelRatio || 1;
            var displayWidth = Math.max(canvas.parentNode.clientWidth - 8, 600);
            var displayHeight = 320;
            canvas.style.width = displayWidth + 'px';
            canvas.style.height = displayHeight + 'px';
            canvas.width = displayWidth * ratio;
            canvas.height = displayHeight * ratio;
            var ctx = canvas.getContext('2d');
            ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
            var W = displayWidth, H = displayHeight;
            var pad = {l: 54, r: 20, t: 26, b: 42};
            ctx.clearRect(0, 0, W, H);
            ctx.fillStyle = '#ffffff';
            ctx.fillRect(0, 0, W, H);

            var max = 0;
            data.forEach(function(d){ max = Math.max(max, +d.visitors || 0, +d.pageviews || 0); });
            max = Math.max(max, 5);

            ctx.strokeStyle = '#e5e7eb';
            ctx.lineWidth = 1;
            for (var i = 0; i < 5; i++) {
                var gy = pad.t + (H - pad.t - pad.b) * (i / 4);
                ctx.beginPath();
                ctx.moveTo(pad.l, gy);
                ctx.lineTo(W - pad.r, gy);
                ctx.stroke();
                var gv = Math.round(max - (max * i / 4));
                ctx.fillStyle = '#6b7280';
                ctx.font = '12px sans-serif';
                ctx.fillText(String(gv), 12, gy + 4);
            }

            function x(i){ return pad.l + (W - pad.l - pad.r) * (i / Math.max(1, data.length - 1)); }
            function y(v){ return H - pad.b - ((v / max) * (H - pad.t - pad.b)); }
            ctx.fillStyle = '#6b7280';
            ctx.font = '12px sans-serif';
            data.forEach(function(d, i){
                var label = (d.date || '').slice(5);
                ctx.fillText(label, x(i) - 16, H - 16);
            });

            function line(key, color){
                ctx.beginPath();
                ctx.strokeStyle = color;
                ctx.lineWidth = 2;
                data.forEach(function(d, i){
                    var vx = x(i), vy = y(+d[key] || 0);
                    if (i === 0) ctx.moveTo(vx, vy); else ctx.lineTo(vx, vy);
                });
                ctx.stroke();
                data.forEach(function(d, i){
                    var vx = x(i), vy = y(+d[key] || 0);
                    ctx.fillStyle = color;
                    ctx.beginPath();
                    ctx.arc(vx, vy, 3.5, 0, Math.PI * 2);
                    ctx.fill();
                });
            }
            line('visitors', '#4f9cf9');
            line('pageviews', '#f59e0b');

            ctx.fillStyle = '#4f9cf9';
            ctx.fillRect(pad.l, 8, 22, 4);
            ctx.fillStyle = '#111827';
            ctx.fillText('访问用户', pad.l + 30, 13);
            ctx.fillStyle = '#f59e0b';
            ctx.fillRect(pad.l + 92, 8, 22, 4);
            ctx.fillStyle = '#111827';
            ctx.fillText('访问次数', pad.l + 122, 13);
        } catch (e) {
            console && console.warn && console.warn('CUOL chart render failed', e);
        }
    }

    drawChart();
    window.addEventListener('resize', function(){
        clearTimeout(window.__cuolChartTimer);
        window.__cuolChartTimer = setTimeout(drawChart, 120);
    });
});

 

checkout-monitor.js文件

(function($){
    'use strict';

    var seenMessages = {};
    var submitTimer = null;

    function getPaymentMethod(){
        var el = document.querySelector('input[name="payment_method"]:checked');
        return el ? el.value : '';
    }

    function getOrderIdFromText(text){
        var match = String(text || '').match(/order[\s#::]*([0-9]+)/i);
        return match ? match[1] : '';
    }

    function normalizeText(text){
        return String(text || '').replace(/\s+/g, ' ').trim();
    }

    function inferActionName(details, fallback){
        var txt = normalizeText(details).toLowerCase();
        if (/card number.*incomplete|your card number is incomplete|卡号.*不完整/.test(txt)) return '银行卡信息不完整';
        if (/security code.*incomplete|cvv|cvc|安全码.*不完整/.test(txt)) return '安全码信息不完整';
        if (/expiration date.*incomplete|expiry|有效期.*不完整/.test(txt)) return '有效期信息不完整';
        if (/network connection error|network error|please refresh and try again|网络.*错误|连接.*错误/.test(txt)) return '网络连接异常';
        return fallback || '前端结账异常';
    }

    function sendLog(payload){
        if (typeof CUOL_Monitor === 'undefined') return;
        payload = payload || {};
        payload.action = 'cuol_log_checkout_issue';
        payload.nonce = CUOL_Monitor.nonce;
        payload.payment_method = payload.payment_method || getPaymentMethod();
        payload.page_url = window.location.href;
        $.post(CUOL_Monitor.ajax_url, payload);
    }

    function logMessage(actionName, details, severity, location, requestUrl){
        details = normalizeText(details);
        if (!details) return;
        actionName = inferActionName(details, actionName);
        var key = [actionName, details, location || ''].join('|');
        if (seenMessages[key]) return;
        seenMessages[key] = true;
        sendLog({
            action_name: actionName,
            severity: severity || 'error',
            location: location || '结账页',
            details: details,
            request_url: requestUrl || window.location.href,
            order_id: getOrderIdFromText(details)
        });
    }

    function collectVisibleErrors(){
        var texts = [];
        [
            '.woocommerce-error',
            'ul.woocommerce-error li',
            '.woocommerce-NoticeGroup-checkout',
            '.woocommerce-notices-wrapper .woocommerce-error',
            '.wc-block-components-notice-banner.is-error',
            '.wc-block-components-notice-banner__content',
            '.shoppe-message, .shoppe-alert, .alert, .notice, .error, .message-error'
        ].forEach(function(sel){
            document.querySelectorAll(sel).forEach(function(node){
                var style = window.getComputedStyle(node);
                if ((style && style.display === 'none') || style.visibility === 'hidden') return;
                var txt = normalizeText(node.innerText || node.textContent || '');
                if (txt && texts.indexOf(txt) === -1) texts.push(txt);
            });
        });
        return texts;
    }

    function scanAndLogVisibleErrors(reason){
        collectVisibleErrors().forEach(function(msg){
            logMessage(reason || '结账页可见报错', msg, 'error', '结账页', window.location.href);
        });
    }

    $(document.body).on('checkout_error updated_checkout', function(e){
        setTimeout(function(){
            scanAndLogVisibleErrors(e && e.type === 'checkout_error' ? '前端结账报错' : '结账页更新后发现错误提示');
        }, 120);
    });

    $(document).on('submit', 'form.checkout, form[name="checkout"]', function(){
        clearTimeout(submitTimer);
        submitTimer = setTimeout(function(){ scanAndLogVisibleErrors('提交结账后页面报错'); }, 1500);
    });

    $(document).ajaxSend(function(event, jqxhr, settings){
        if (!settings || !settings.url) return;
        if (settings.url.indexOf('wc-ajax=checkout') !== -1) {
            clearTimeout(submitTimer);
            submitTimer = setTimeout(function(){
                logMessage('结账接口长时间无响应', '结账请求发出后较长时间未正常完成,可能是网络超时、网关阻塞或脚本中断。', 'warning', '结账页AJAX', settings.url);
            }, 20000);
        }
    });

    $(document).ajaxError(function(event, jqxhr, settings, thrownError){
        if (!settings || !settings.url) return;
        if (settings.url.indexOf('wc-ajax=checkout') === -1 && settings.url.indexOf('/checkout') === -1) return;
        clearTimeout(submitTimer);
        var errorText = thrownError || (jqxhr ? jqxhr.statusText : '');
        logMessage('网络连接异常', errorText || '结账接口请求失败,可能是网络异常、接口超时、跨域拦截或支付网关未正常返回。', 'error', '结账页AJAX', settings.url);
        sendLog({
            action_name: '网络连接异常',
            severity: 'error',
            location: '结账页AJAX',
            details: '结账接口请求失败,可能是网络异常、接口超时、跨域拦截或支付网关未正常返回。',
            request_url: settings.url,
            http_status: jqxhr ? jqxhr.status : '',
            error_text: errorText
        });
        setTimeout(function(){ scanAndLogVisibleErrors('结账失败后页面提示'); }, 200);
    });

    $(document).ajaxComplete(function(event, xhr, settings){
        if (!settings || !settings.url || settings.url.indexOf('wc-ajax=checkout') === -1) return;
        clearTimeout(submitTimer);
        try {
            var response = xhr && xhr.responseText ? JSON.parse(xhr.responseText) : null;
            if (response && response.result === 'failure') {
                var message = '';
                if (response.messages) message = $('<div>').html(response.messages).text().trim();
                sendLog({
                    action_name: inferActionName(message, '结账接口返回失败'),
                    severity: 'warning',
                    location: '结账页AJAX',
                    details: message || '结账接口返回 result=failure,但未提取到明确提示信息。',
                    request_url: settings.url,
                    http_status: xhr.status,
                    order_id: getOrderIdFromText(message)
                });
            }
        } catch(e) {
            var rawText = normalizeText(xhr && xhr.responseText ? xhr.responseText.slice(0, 500) : '');
            sendLog({
                action_name: inferActionName(rawText, '结账接口返回异常内容'),
                severity: 'warning',
                location: '结账页AJAX',
                details: rawText || '结账接口返回内容无法解析为 JSON,可能是网关输出了非标准响应。',
                request_url: settings.url,
                http_status: xhr ? xhr.status : '',
                error_text: e && e.message ? e.message : 'JSON parse error'
            });
        }
        setTimeout(function(){ scanAndLogVisibleErrors('结账结果页面提示'); }, 200);
    });

    function watchDomErrors(){
        var root = document.body;
        if (!root || typeof MutationObserver === 'undefined') return;
        var observer = new MutationObserver(function(mutations){
            var shouldScan = false;
            mutations.forEach(function(m){
                if (shouldScan) return;
                if (m.type === 'childList' && m.addedNodes && m.addedNodes.length) shouldScan = true;
                if (m.type === 'attributes') shouldScan = true;
            });
            if (shouldScan) setTimeout(function(){ scanAndLogVisibleErrors('页面动态提示报错'); }, 100);
        });
        observer.observe(root, {childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style']});
    }

    $(function(){
        watchDomErrors();
        setTimeout(function(){ scanAndLogVisibleErrors('结账页初始错误提示'); }, 500);
        window.addEventListener('error', function(e){
            var msg = e && e.message ? e.message : '';
            if (msg) logMessage('前端脚本异常', msg, 'warning', '结账页脚本', window.location.href);
        });
        window.addEventListener('unhandledrejection', function(e){
            var reason = e && e.reason ? (e.reason.message || e.reason.toString()) : '';
            if (reason) logMessage('前端 Promise 异常', reason, 'warning', '结账页脚本', window.location.href);
        });
    });
})(jQuery);

 

runtime.js文件

(function($){
    'use strict';

    function getVisitorKey(){
        try {
            var key = window.localStorage ? localStorage.getItem('cuol_vid') : '';
            if(!key){
                key = 'cv_' + Math.random().toString(36).slice(2,10) + Date.now().toString(36);
                if(window.localStorage){ localStorage.setItem('cuol_vid', key); }
            }
            return key;
        } catch(e){
            return 'cv_' + Math.random().toString(36).slice(2,10) + Date.now().toString(36);
        }
    }

    function payload(){
        return {
            action:'cuol_ping_visitor',
            nonce:CUOL_Runtime.nonce,
            page_url:window.location.href,
            visitor_key:getVisitorKey()
        };
    }

    function updateCounts(data){
        if(!data){return;}
        if(document.getElementById('cuol-adminbar-online-count')) document.getElementById('cuol-adminbar-online-count').textContent = data.online;
        if(document.getElementById('cuol-live-online')) document.getElementById('cuol-live-online').textContent = data.online;
        if(document.getElementById('cuol-live-today')) document.getElementById('cuol-live-today').textContent = data.today;
        if(document.getElementById('cuol-live-yesterday')) document.getElementById('cuol-live-yesterday').textContent = data.yesterday;
        if(document.getElementById('cuol-live-pageviews')) document.getElementById('cuol-live-pageviews').textContent = data.pageviews_today;
    }

    function ping(){
        if(typeof CUOL_Runtime === 'undefined'){return;}
        return $.post(CUOL_Runtime.ajax_url, payload()).done(function(resp){
            if(resp && resp.success && resp.data && typeof resp.data.online !== 'undefined'){
                updateCounts(resp.data);
            }
        });
    }

    function refreshCounts(){
        if(typeof CUOL_Runtime === 'undefined'){return;}
        return $.post(CUOL_Runtime.ajax_url,{action:'cuol_get_live_counts',nonce:CUOL_Runtime.nonce},function(resp){
            if(!resp || !resp.success || !resp.data){return;}
            updateCounts(resp.data);
        });
    }

    function fullRefresh(){
        ping();
        setTimeout(refreshCounts, 300);
    }

    $(function(){
        fullRefresh();
        setInterval(fullRefresh, (CUOL_Runtime && CUOL_Runtime.refresh ? CUOL_Runtime.refresh : 10) * 1000);
        $(window).on('focus pageshow', fullRefresh);
        document.addEventListener('visibilitychange', function(){
            if(document.visibilityState === 'visible'){ fullRefresh(); }
        });
    });
})(jQuery);

 

admin.css文件

/* ========== 基础容器 ========== */
.cuol-wrap {
    margin-top: 20px;
}

.cuol-header-card,
.cuol-filter-card,
.cuol-table-card {
    background: #fff;
    border: 1px solid #e8e8ee;
    border-radius: 16px;
    padding: 20px;
    box-shadow: 0 6px 18px rgba(18, 38, 63, 0.04);
    margin-bottom: 18px;
}

/* ========== 顶部头部 ========== */
.cuol-header-card {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 20px;
}

.cuol-header-card h1 {
    margin: 0 0 6px;
    font-size: 24px;
}

.cuol-header-card p {
    margin: 0;
    color: #596780;
}

.cuol-top-actions {
    display: flex;
    gap: 10px;
    align-items: center;
    flex-wrap: wrap;
}

.cuol-top-actions form {
    margin: 0;
}

.cuol-danger-btn {
    border-color: #d63638 !important;
    color: #d63638 !important;
}

/* ========== 筛选区域 ========== */
.cuol-filter-form {
    display: grid;
    grid-template-columns: 2fr 1fr 1fr auto;
    gap: 14px;
    align-items: end;
}

.cuol-filter-form-6 {
    grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1fr 1fr auto;
}

.cuol-filter-actions {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
}

/* ========== 表单字段 ========== */
.cuol-field-group label,
.cuol-field-inline label {
    display: block;
    font-weight: 600;
    margin-bottom: 8px;
    color: #1f2937;
}

.cuol-field-group input,
.cuol-field-group select,
.cuol-field-inline input {
    width: 100%;
    min-height: 40px;
    border: 1px solid #d9deea;
    border-radius: 10px;
    padding: 0 12px;
}

.cuol-field-inline input {
    width: 120px;
}

/* ========== 统计卡片 ========== */
.cuol-stats-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 14px;
    margin-bottom: 18px;
}

.cuol-stats-grid-5 {
    grid-template-columns: repeat(5, 1fr);
}

.cuol-stats-grid-4 {
    grid-template-columns: repeat(4, 1fr);
}

.cuol-stat-box {
    background: linear-gradient(180deg, #ffffff 0%, #f7f9fc 100%);
    border: 1px solid #e5eaf3;
    border-radius: 14px;
    padding: 18px;
    display: block;
    text-decoration: none;
}

.cuol-stat-link:hover {
    transform: translateY(-1px);
    box-shadow: 0 8px 20px rgba(18, 38, 63, 0.08);
}

.cuol-stat-box strong {
    display: block;
    font-size: 26px;
    line-height: 1.2;
    color: #111827;
}

.cuol-stat-box span {
    display: block;
    margin-top: 8px;
    color: #6b7280;
}

.cuol-live-box {
    background: linear-gradient(180deg, #e8fff8 0%, #f7fffc 100%);
}

/* ========== 设置区域 ========== */
.cuol-settings-grid {
    display: grid;
    grid-template-columns: 2fr 280px;
    gap: 24px;
    align-items: start;
}

.cuol-settings-form-block {
    margin: 0;
}

.cuol-settings-side {
    display: flex;
    flex-direction: column;
    gap: 10px;
    align-items: flex-start;
}

.cuol-checkbox-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(220px, 1fr));
    gap: 12px 18px;
    margin-top: 12px;
}

.cuol-checkbox-item {
    display: flex;
    align-items: center;
    gap: 10px;
    font-weight: 600;
    color: #1f2937;
}

.cuol-checkbox-item input {
    margin: 0;
}

/* ========== 通用文字 ========== */
.cuol-section-title {
    margin: 0 0 10px;
    font-size: 18px;
}

.cuol-muted {
    color: #6b7280;
    margin: 6px 0 0;
}

.cuol-subtext {
    color: #6b7280;
    font-size: 12px;
    margin-top: 4px;
    line-height: 1.5;
}

.cuol-panel-title {
    font-size: 16px;
    font-weight: 700;
    margin-bottom: 14px;
    color: #111827;
}

.cuol-empty {
    text-align: center;
    padding: 36px 12px;
    color: #6b7280;
}

/* ========== 批量操作栏 ========== */
.cuol-bulk-bar {
    display: flex;
    gap: 10px;
    align-items: center;
    margin-bottom: 14px;
    flex-wrap: wrap;
}

.cuol-bulk-bar select {
    min-width: 130px;
    min-height: 36px;
    padding: 0 10px;
    border: 1px solid #d9deea;
    border-radius: 10px;
}

/* ========== 表格 ========== */
.cuol-table th,
.cuol-table td {
    vertical-align: top !important;
    padding-top: 14px !important;
    padding-bottom: 14px !important;
}

.cuol-location,
.cuol-details {
    max-width: 320px;
    white-space: normal;
    line-height: 1.6;
    color: #111827;
    word-break: break-word;
}

.cuol-location a {
    word-break: break-all;
}

/* ========== 徽章 ========== */
.cuol-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 4px 10px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 700;
}

.cuol-badge.info {
    background: #e8f3ff;
    color: #0c63e7;
}

.cuol-badge.warning {
    background: #fff7df;
    color: #9a6700;
}

.cuol-badge.error {
    background: #ffe7e7;
    color: #c62828;
}

.cuol-badge.module {
    background: #f2f4f8;
    color: #344054;
}

/* ========== 附加详情 ========== */
.cuol-extra-details {
    margin-top: 8px;
}

.cuol-extra-details summary {
    cursor: pointer;
    color: #2271b1;
}

.cuol-extra-details pre {
    background: #0f172a;
    color: #e5eefb;
    padding: 12px;
    border-radius: 10px;
    overflow: auto;
    white-space: pre-wrap;
    word-break: break-word;
    margin-top: 8px;
}

/* ========== 图表区域 ========== */
.cuol-chart-grid {
    display: grid;
    grid-template-columns: 2fr 1fr;
    gap: 18px;
}

#cuol-chart {
    display: block;
    width: 100%;
    max-width: 100%;
    min-height: 320px;
    background: #fff;
    border-radius: 12px;
}

/* ========== 顶部工具栏在线人数 ========== */
.cuol-adminbar-online .ab-item {
    font-weight: 700;
}

/* ========== 表格底部 / 分页 ========== */
.cuol-table-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
    padding-top: 16px;
}

.cuol-per-page-form {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

.cuol-per-page-form select {
    min-width: 90px;
    min-height: 36px;
    padding: 0 10px;
    border: 1px solid #d9deea;
    border-radius: 10px;
}

.cuol-pagination-wrap {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    justify-content: flex-end;
    margin-left: auto;
}

.cuol-pagination-wrap .page-numbers {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 34px;
    height: 34px;
    padding: 0 10px;
    border: 1px solid #d9deea;
    border-radius: 8px;
    text-decoration: none;
    background: #fff;
}

.cuol-pagination-wrap .page-numbers.current {
    background: #2271b1;
    color: #fff;
    border-color: #2271b1;
}

.cuol-pagination-wrap .page-numbers.dots {
    border: none;
    background: transparent;
    min-width: auto;
}

/* ========== 响应式 ========== */
@media (max-width: 1480px) {
    .cuol-stats-grid-5 {
        grid-template-columns: repeat(3, 1fr);
    }

    .cuol-filter-form-6 {
        grid-template-columns: repeat(4, 1fr);
    }

    .cuol-chart-grid {
        grid-template-columns: 1fr;
    }
}

@media (max-width: 1280px) {
    .cuol-stats-grid {
        grid-template-columns: repeat(2, 1fr);
    }

    .cuol-filter-form {
        grid-template-columns: 1fr 1fr;
    }

    .cuol-settings-grid {
        grid-template-columns: 1fr;
    }
}

@media (max-width: 782px) {
    .cuol-header-card {
        flex-direction: column;
        align-items: flex-start;
    }

    .cuol-stats-grid,
    .cuol-filter-form,
    .cuol-filter-form-6,
    .cuol-chart-grid,
    .cuol-checkbox-grid {
        grid-template-columns: 1fr;
    }

    .cuol-location,
    .cuol-details {
        max-width: none;
    }

    .cuol-table-footer {
        flex-direction: column;
        align-items: flex-start;
    }

    .cuol-pagination-wrap {
        margin-left: 0;
        justify-content: flex-start;
    }
}

 

 

插件下载

通过网盘分享的文件:cn-user-ops-logger.zip
链接: https://pan.baidu.com/s/1CK3VK2DWuGlp3mX1iu4ivw?pwd=sgvd 提取码: sgvd 复制这段内容后打开百度网盘手机App,操作更方便哦

 

posted @ 2026-03-07 15:47  还好阿卡  阅读(5)  评论(0)    收藏  举报