get_col($wpdb->prepare( "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND CAST(meta_value AS UNSIGNED) < %d", self::META_CHANGED_AT, $cutoff )); if (empty($rows)) return; foreach ($rows as $uid) { delete_user_meta((int) $uid, self::META_PREVIOUS); delete_user_meta((int) $uid, self::META_CHANGED_AT); } } /** * 在 14 天窗口内? */ public static function is_within_window(int $user_id): bool { $changed_at = (int) get_user_meta($user_id, self::META_CHANGED_AT, true); if ($changed_at <= 0) return false; return (time() - $changed_at) <= self::WINDOW_SECONDS; } /** * 列出窗口内可回滚的用户。 */ public static function list_pending(int $page = 1, int $per_page = 20): array { global $wpdb; $page = max(1, $page); $per_page = min(100, max(1, $per_page)); $offset = ($page - 1) * $per_page; $cutoff = time() - self::WINDOW_SECONDS; $sql_count = $wpdb->prepare( "SELECT COUNT(DISTINCT um1.user_id) FROM {$wpdb->usermeta} um1 INNER JOIN {$wpdb->usermeta} um2 ON um2.user_id = um1.user_id AND um2.meta_key = %s WHERE um1.meta_key = %s AND CAST(um1.meta_value AS UNSIGNED) >= %d", self::META_PREVIOUS, self::META_CHANGED_AT, $cutoff ); $total = (int) $wpdb->get_var($sql_count); $sql = $wpdb->prepare( "SELECT um1.user_id, um1.meta_value AS changed_at, um2.meta_value AS previous_email FROM {$wpdb->usermeta} um1 INNER JOIN {$wpdb->usermeta} um2 ON um2.user_id = um1.user_id AND um2.meta_key = %s WHERE um1.meta_key = %s AND CAST(um1.meta_value AS UNSIGNED) >= %d ORDER BY CAST(um1.meta_value AS UNSIGNED) DESC LIMIT %d OFFSET %d", self::META_PREVIOUS, self::META_CHANGED_AT, $cutoff, $per_page, $offset ); $rows = $wpdb->get_results($sql, ARRAY_A); $items = []; foreach ((array) $rows as $r) { $uid = (int) $r['user_id']; $u = get_userdata($uid); if (!$u) continue; $items[] = [ 'user_id' => $uid, 'login' => $u->user_login, 'display_name' => $u->display_name, 'current_email' => $u->user_email, 'previous_email' => (string) $r['previous_email'], 'changed_at' => (int) $r['changed_at'], 'expires_in' => max(0, (int) $r['changed_at'] + self::WINDOW_SECONDS - time()), ]; } return [ 'items' => $items, 'total' => $total, 'page' => $page, 'per_page' => $per_page, ]; } /** * 执行回滚。 * * @return array{success:bool, message?:string, error?:string} */ public static function rollback(int $user_id, int $by_admin_id = 0): array { if ($user_id <= 0 || !current_user_can('edit_user', $user_id)) { return ['success' => false, 'error' => __('权限不足', 'aksrt')]; } if (!self::is_within_window($user_id)) { return ['success' => false, 'error' => __('回滚窗口已过期或当前账号未发起过邮箱变更', 'aksrt')]; } $previous = (string) get_user_meta($user_id, self::META_PREVIOUS, true); if ($previous === '' || !is_email($previous)) { return ['success' => false, 'error' => __('旧邮箱无效', 'aksrt')]; } // 检查旧邮箱是否被其他用户占用(理论上极少见) $occupier = email_exists($previous); if ($occupier && (int) $occupier !== $user_id) { return ['success' => false, 'error' => __('旧邮箱已被其他账号占用,无法回滚', 'aksrt')]; } $update = wp_update_user([ 'ID' => $user_id, 'user_email' => $previous, ]); if (is_wp_error($update)) { return ['success' => false, 'error' => $update->get_error_message()]; } // 清空回滚 meta(一次性) delete_user_meta($user_id, self::META_PREVIOUS); delete_user_meta($user_id, self::META_CHANGED_AT); // 记录管理员操作日志 $log = get_user_meta($user_id, 'aksrt_email_verification_admin_log', true); if (!is_array($log)) $log = []; $log[] = [ 't' => time(), 'action' => 'email_rollback', 'by' => $by_admin_id ?: get_current_user_id(), ]; if (count($log) > 10) $log = array_slice($log, -10); update_user_meta($user_id, 'aksrt_email_verification_admin_log', $log); if (class_exists('AKSRT_Auth_Service')) { AKSRT_Auth_Service::log_event('admin.email_rollback', [ 'uid' => $user_id, 'by' => $by_admin_id ?: get_current_user_id(), ]); } return [ 'success' => true, 'message' => sprintf(__('已将用户邮箱回滚为 %s', 'aksrt'), $previous), ]; } /** * 在用户列表行操作中加入"回滚邮箱"。 */ public function add_row_action(array $actions, WP_User $user): array { if (!current_user_can('edit_user', $user->ID)) return $actions; if (!self::is_within_window((int) $user->ID)) return $actions; $url = wp_nonce_url( admin_url('admin-post.php?action=aksrt_email_rollback&user_id=' . (int) $user->ID), 'aksrt_email_rollback_' . (int) $user->ID ); $actions['aksrt_email_rollback'] = sprintf( '%s', esc_url($url), esc_html__('回滚邮箱', 'aksrt') ); return $actions; } public function handle_admin_rollback(): void { $user_id = isset($_GET['user_id']) ? absint($_GET['user_id']) : 0; if ($user_id <= 0 || !current_user_can('edit_user', $user_id)) { wp_die(esc_html__('权限不足', 'aksrt')); } check_admin_referer('aksrt_email_rollback_' . $user_id); $r = self::rollback($user_id, get_current_user_id()); $type = !empty($r['success']) ? 'success' : 'error'; $msg = $r['message'] ?? $r['error'] ?? ''; set_transient('aksrt_admin_notice_' . get_current_user_id(), [['type' => $type, 'msg' => $msg]], 30); wp_safe_redirect(wp_get_referer() ?: admin_url('users.php')); exit; } }