WordPress日志记录与访客人数
效果图



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

下面是插件目录以及源码

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,操作更方便哦

浙公网安备 33010602011771号