ID); } public static function is_email_verified(int $user_id): bool { return (int) get_user_meta($user_id, Service::EMAIL_VERIFIED_META, true) === 1; } public static function mark_email_verified(int $user_id): void { $was_verified = (int) get_user_meta($user_id, Service::EMAIL_VERIFIED_META, true) === 1; update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 1); self::clear_email_verification($user_id); // 仅在状态由"未验证 → 已验证"跳变时触发,避免历史 backfill / 重复调用造成奖励重发。 // 监听方负责自己幂等(aksrt_invite 表的 status 检查已经做到这一点)。 if (!$was_verified) { do_action('aksrt_email_verified', $user_id); } } public static function clear_email_verification(int $user_id): void { delete_user_meta($user_id, Service::EMAIL_METHOD_META); delete_user_meta($user_id, Service::EMAIL_TOKEN_HASH_META); delete_user_meta($user_id, Service::EMAIL_CODE_HASH_META); delete_user_meta($user_id, Service::EMAIL_EXPIRES_META); delete_user_meta($user_id, Service::EMAIL_REQUESTED_AT_META); delete_user_meta($user_id, Service::EMAIL_TARGET_META); delete_user_meta($user_id, Service::EMAIL_OBK_META); self::reset_email_code_attempt_trackers($user_id); } // ============================================================ // 公开 API:管理员后台 / 内部业务调用 // ============================================================ /** * 是否已验证邮箱(公开版本,给后台 UI / 用户列表渲染用)。 */ public static function user_is_email_verified(int $user_id): bool { return self::is_email_verified($user_id); } /** * 取一个用户当前的验证状态摘要,用于后台资料页展示。 * * @return array{verified:bool, method:string, target_email:string, expires_at:int, requested_at:int, last_admin_log:array} */ public static function get_user_verification_state(int $user_id): array { return [ 'verified' => self::is_email_verified($user_id), 'method' => (string) get_user_meta($user_id, Service::EMAIL_METHOD_META, true), 'target_email' => (string) get_user_meta($user_id, Service::EMAIL_TARGET_META, true), 'expires_at' => (int) get_user_meta($user_id, Service::EMAIL_EXPIRES_META, true), 'requested_at' => (int) get_user_meta($user_id, Service::EMAIL_REQUESTED_AT_META, true), 'last_admin_log' => (array) (get_user_meta($user_id, 'aksrt_email_verification_admin_log', true) ?: []), ]; } /** * 管理员:把指定用户标记为已验证(清除所有进行中的验证 meta)。 */ public static function admin_mark_verified(int $user_id, int $by_admin_id = 0): array { if ($user_id <= 0) { return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')]; } if (!current_user_can('edit_user', $user_id)) { return ['success' => false, 'error' => __('权限不足', 'aksrt')]; } self::mark_email_verified($user_id); self::record_admin_action($user_id, 'mark_verified', $by_admin_id); Service::log_event('admin.action', ['action' => 'mark_verified', 'uid' => $user_id, 'by' => $by_admin_id]); return ['success' => true, 'message' => __('已标记为已验证', 'aksrt')]; } /** * 管理员:撤销已验证状态(用于测试 / 回滚 / 应对邮箱泄漏)。 */ public static function admin_unmark_verified(int $user_id, int $by_admin_id = 0): array { if ($user_id <= 0) { return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')]; } if (!current_user_can('edit_user', $user_id)) { return ['success' => false, 'error' => __('权限不足', 'aksrt')]; } update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 0); self::clear_email_verification($user_id); self::record_admin_action($user_id, 'unmark_verified', $by_admin_id); Service::log_event('admin.action', ['action' => 'unmark_verified', 'uid' => $user_id, 'by' => $by_admin_id]); return ['success' => true, 'message' => __('已撤销验证状态', 'aksrt')]; } /** * 管理员:立即发送 / 重发验证邮件(绕过 60s 节流)。 */ public static function admin_send_verification(int $user_id, int $by_admin_id = 0): array { if ($user_id <= 0) { return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')]; } if (!current_user_can('edit_user', $user_id)) { return ['success' => false, 'error' => __('权限不足', 'aksrt')]; } $user = get_userdata($user_id); if (!$user || is_wp_error($user)) { return ['success' => false, 'error' => __('用户不存在', 'aksrt')]; } $method = Service::current_registration_verification_method(); $email = (string) get_user_meta($user_id, Service::EMAIL_TARGET_META, true); if (!in_array($method, ['link', 'code'], true)) { $method = \AKSRT\Settings\Settings::registration_verification_method(); } if ($email === '' || !is_email($email)) { $email = (string) $user->user_email; } $result = self::begin_email_verification($user, $method, [ 'email' => $email, 'obk' => (string) get_user_meta($user_id, Service::EMAIL_OBK_META, true), 'bypass_throttle' => true, 'force' => true, ]); // 标记为待验证(确保 admin 主动发邮件后用户被视为未验证) if (!empty($result['success'])) { update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 0); self::record_admin_action($user_id, 'send_verification', $by_admin_id); Service::log_event('admin.action', ['action' => 'send_verification', 'uid' => $user_id, 'by' => $by_admin_id]); } return $result; } /** * 把已存在的用户视为受信任(grandfather):写入 aksrt_email_verified=1。 * * 仅写入 meta 不存在的用户;已经显式为 0 的(正在走验证流程的新注册)保持不动。 * * 用户量 > 50000 时改为异步分批;否则内联完成。返回处理总数。 * * @param bool $force 强制再次执行(即便 backfill_done 标记已存在),调试用 */ public static function backfill_legacy_users_as_verified(bool $force = false): int { if (!$force && get_option('aksrt_email_verification_backfill_done')) { return 0; } $total = 0; if (function_exists('count_users')) { $counts = count_users(); $total = (int) ($counts['total_users'] ?? 0); } // 大表保护:超过 5w 用户改为后台 cron 分批 if ($total > 50000) { if (!wp_next_scheduled('aksrt_email_verification_backfill_batch')) { wp_schedule_single_event(time() + 30, 'aksrt_email_verification_backfill_batch', [0]); } update_option('aksrt_email_verification_backfill_log', [ 'mode' => 'async', 'total' => $total, 'scheduled' => time(), 'by' => get_current_user_id(), ]); return 0; } // 内联分批,每批 500 $updated = 0; $paged = 1; do { $users = get_users([ 'number' => 500, 'paged' => $paged, 'fields' => ['ID'], 'meta_query' => [ [ 'key' => Service::EMAIL_VERIFIED_META, 'compare' => 'NOT EXISTS', ], ], ]); if (empty($users)) { break; } foreach ($users as $u) { update_user_meta((int) $u->ID, Service::EMAIL_VERIFIED_META, 1); $updated++; } $paged++; // 让 MySQL 喘口气 if (function_exists('usleep')) { usleep(50000); } } while (count($users) === 500 && $paged < 200); // 上限 10w 兜底防呆 update_option('aksrt_email_verification_backfill_done', time()); update_option('aksrt_email_verification_backfill_log', [ 'mode' => 'inline', 'count' => $updated, 'ran_at' => time(), 'by' => get_current_user_id(), ]); Service::log_event('admin.backfill', ['count' => $updated, 'by' => get_current_user_id()]); return $updated; } /** * 异步分批回填:cron 钩子。每批 500,自动调度下一批直到完成。 */ public static function run_async_backfill_batch(int $offset = 0): void { $users = get_users([ 'number' => 500, 'offset' => max(0, $offset), 'fields' => ['ID'], 'orderby' => 'ID', 'order' => 'ASC', 'meta_query' => [ [ 'key' => Service::EMAIL_VERIFIED_META, 'compare' => 'NOT EXISTS', ], ], ]); if (empty($users)) { update_option('aksrt_email_verification_backfill_done', time()); $log = (array) get_option('aksrt_email_verification_backfill_log', []); $log['finished_at'] = time(); update_option('aksrt_email_verification_backfill_log', $log); Service::log_event('admin.backfill.finished', []); return; } foreach ($users as $u) { update_user_meta((int) $u->ID, Service::EMAIL_VERIFIED_META, 1); } // 调度下一批 wp_schedule_single_event(time() + 30, 'aksrt_email_verification_backfill_batch', [$offset + 500]); } /** * 记录管理员操作日志(每个用户最多 10 条 ring buffer)。 */ public static function record_admin_action(int $user_id, string $action, int $by_admin_id): void { $log = get_user_meta($user_id, 'aksrt_email_verification_admin_log', true); if (!is_array($log)) { $log = []; } $log[] = [ 't' => time(), 'action' => $action, '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); } // ============================================================ // 验证码失败计数 / IP 节流 // ============================================================ public static function email_code_ip_throttle_key(int $user_id): string { $ip = sanitize_text_field((string) ($_SERVER['REMOTE_ADDR'] ?? '')); if ($user_id <= 0 || $ip === '') { return ''; } return 'aksrt_email_code_try_' . $user_id . '_' . md5($ip); } public static function reset_email_code_attempt_trackers(int $user_id): void { delete_user_meta($user_id, Service::EMAIL_CODE_ATTEMPTS_META); $ip_key = self::email_code_ip_throttle_key($user_id); if ($ip_key !== '') { delete_transient($ip_key); } } public static function fail_email_code_attempt(int $user_id): array { $attempts = (int) get_user_meta($user_id, Service::EMAIL_CODE_ATTEMPTS_META, true); $attempts++; update_user_meta($user_id, Service::EMAIL_CODE_ATTEMPTS_META, $attempts); $ip_attempts = 0; $ip_key = self::email_code_ip_throttle_key($user_id); if ($ip_key !== '') { $ip_attempts = (int) get_transient($ip_key); $ip_attempts++; set_transient($ip_key, $ip_attempts, Service::EMAIL_CODE_IP_TTL); } if ($attempts >= Service::EMAIL_CODE_MAX_ATTEMPTS || $ip_attempts >= Service::EMAIL_CODE_MAX_ATTEMPTS) { delete_user_meta($user_id, Service::EMAIL_CODE_HASH_META); update_user_meta($user_id, Service::EMAIL_REQUESTED_AT_META, 0); Service::log_event('verify.code.locked', ['uid' => $user_id, 'attempts' => $attempts]); return ['success' => false, 'error' => __('验证码错误次数过多,当前验证码已失效,请重新发送。', 'aksrt')]; } Service::log_event('verify.code.fail', ['uid' => $user_id, 'attempts' => $attempts]); return ['success' => false, 'error' => __('邮箱验证码错误或已过期,请重新输入。', 'aksrt')]; } // ============================================================ // 验证流程:发起 / 校验 / 完成 // ============================================================ public static function begin_email_verification(\WP_User $user, string $method, array $args = []): array { $method = in_array($method, ['link', 'code'], true) ? $method : 'link'; $email = sanitize_email((string) ($args['email'] ?? $user->user_email)); $obk = sanitize_text_field((string) ($args['obk'] ?? '')); $bypass_throttle = !empty($args['bypass_throttle']); if ($email === '' || !is_email($email)) { return ['success' => false, 'error' => __('当前账号缺少有效邮箱,无法发送验证邮件', 'aksrt')]; } // 节流:仅在非管理员路径生效(admin_send_verification 会传 bypass_throttle=true) if (!$bypass_throttle) { $last_requested = (int) get_user_meta($user->ID, Service::EMAIL_REQUESTED_AT_META, true); if ($last_requested > 0 && (time() - $last_requested) < 60 && empty($args['force'])) { return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')]; } } $expires_at = time() + (\AKSRT\Settings\Settings::registration_verification_ttl_minutes() * MINUTE_IN_SECONDS); update_user_meta($user->ID, Service::EMAIL_VERIFIED_META, 0); update_user_meta($user->ID, Service::EMAIL_METHOD_META, $method); update_user_meta($user->ID, Service::EMAIL_TARGET_META, $email); update_user_meta($user->ID, Service::EMAIL_EXPIRES_META, $expires_at); update_user_meta($user->ID, Service::EMAIL_REQUESTED_AT_META, time()); if ($obk !== '') { update_user_meta($user->ID, Service::EMAIL_OBK_META, $obk); } delete_user_meta($user->ID, Service::EMAIL_TOKEN_HASH_META); delete_user_meta($user->ID, Service::EMAIL_CODE_HASH_META); if ($method === 'code') { $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); update_user_meta($user->ID, Service::EMAIL_CODE_HASH_META, wp_hash_password($code)); // 新验证码 = 重置失败计数 / IP 节流,避免被旧计数立即锁死 self::reset_email_code_attempt_trackers((int) $user->ID); $sent = self::send_registration_verification_code_email($user, $email, $code); } else { $token = wp_generate_password(48, false, false); update_user_meta($user->ID, Service::EMAIL_TOKEN_HASH_META, hash('sha256', $token)); $sent = self::send_registration_verification_link_email($user, $email, $token); } if (!$sent) { self::clear_email_verification((int) $user->ID); Service::log_event('email.send.fail', ['uid' => (int) $user->ID, 'method' => $method]); return ['success' => false, 'error' => __('验证邮件发送失败,请稍后重试', 'aksrt')]; } Service::log_event('email.send.ok', ['uid' => (int) $user->ID, 'method' => $method]); $message = $method === 'code' ? __('注册成功,验证码已发送到你的邮箱,请完成验证后继续。', 'aksrt') : __('注册成功,验证邮件已发送到你的邮箱,请点击邮件中的链接完成验证。', 'aksrt'); return [ 'success' => true, 'message' => $message, ]; } public static function verify_email_token(int $user_id, string $token): array { $user = get_userdata($user_id); if (!$user || is_wp_error($user)) { return ['success' => false, 'error' => __('验证账号不存在或已失效', 'aksrt')]; } $stored_hash = (string) get_user_meta($user_id, Service::EMAIL_TOKEN_HASH_META, true); $expires_at = (int) get_user_meta($user_id, Service::EMAIL_EXPIRES_META, true); if ($stored_hash === '' || $expires_at <= time() || !hash_equals($stored_hash, hash('sha256', $token))) { return ['success' => false, 'error' => __('验证链接无效或已过期,请重新发送验证邮件。', 'aksrt')]; } return self::complete_email_verification($user); } public static function verify_email_code(int $user_id, string $code): array { $user = get_userdata($user_id); if (!$user || is_wp_error($user)) { return ['success' => false, 'error' => __('验证账号不存在或已失效', 'aksrt')]; } // 先做 IP 节流:相同 IP 在 EMAIL_CODE_IP_TTL 内累计失败到上限即直接拒绝 $ip_key = self::email_code_ip_throttle_key($user_id); if ($ip_key !== '') { $ip_attempts = (int) get_transient($ip_key); if ($ip_attempts >= Service::EMAIL_CODE_MAX_ATTEMPTS) { Service::log_event('verify.code.ip_throttled', ['uid' => $user_id, 'attempts' => $ip_attempts]); return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')]; } } // 用户级失败计数:达到上限后该验证码立即作废 $user_attempts = (int) get_user_meta($user_id, Service::EMAIL_CODE_ATTEMPTS_META, true); if ($user_attempts >= Service::EMAIL_CODE_MAX_ATTEMPTS) { delete_user_meta($user_id, Service::EMAIL_CODE_HASH_META); update_user_meta($user_id, Service::EMAIL_REQUESTED_AT_META, 0); Service::log_event('verify.code.locked.read', ['uid' => $user_id]); return ['success' => false, 'error' => __('验证码错误次数过多,当前验证码已失效,请重新发送。', 'aksrt')]; } $stored_hash = (string) get_user_meta($user_id, Service::EMAIL_CODE_HASH_META, true); $expires_at = (int) get_user_meta($user_id, Service::EMAIL_EXPIRES_META, true); if ($stored_hash === '' || $expires_at <= time() || !wp_check_password($code, $stored_hash)) { return self::fail_email_code_attempt($user_id); } self::reset_email_code_attempt_trackers($user_id); return self::complete_email_verification($user); } public static function complete_email_verification(\WP_User $user): array { $user_id = (int) $user->ID; $obk = (string) get_user_meta($user_id, Service::EMAIL_OBK_META, true); $oauth = $obk !== '' ? Service::get_oauth_bind_context($obk) : null; $email = (string) get_user_meta($user_id, Service::EMAIL_TARGET_META, true); $was_verified = (int) get_user_meta($user_id, Service::EMAIL_VERIFIED_META, true) === 1; update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 1); self::clear_email_verification($user_id); Service::log_event('verify.success', ['uid' => $user_id]); // 状态跳变时触发 hook,邀请系统等模块据此发奖 if (!$was_verified) { do_action('aksrt_email_verified', $user_id); } if ($email !== '' && is_email($email)) { self::send_registration_verified_notice_email($user, $email); } if ($oauth && is_array($oauth)) { Service::finalize_oauth_registration($user_id, $oauth, $obk); } elseif (\AKSRT\Settings\Settings::registration_auto_login_after_verify()) { wp_set_current_user($user_id); wp_set_auth_cookie($user_id, true); } $redirect = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/'); if (!\AKSRT\Settings\Settings::registration_auto_login_after_verify()) { $redirect = aksrt_get_login_url(); } return [ 'success' => true, 'message' => __('邮箱验证成功,即将进入你的账号。', 'aksrt'), 'redirect' => $redirect, ]; } // ============================================================ // 通知邮件 // ============================================================ public static function send_registration_verification_link_email(\WP_User $user, string $email, string $token): bool { $url = Service::get_email_verification_url((int) $user->ID, ['token' => $token]); $site_name = get_bloginfo('name'); $default_subject = '[' . $site_name . '] ' . __('验证你的邮箱地址', 'aksrt'); $default_content = sprintf( __("您好 %s,\n\n欢迎注册 %s。\n\n请点击以下链接完成邮箱验证:\n\n%s\n\n验证成功后系统将自动为你登录。\n\n如果这不是你的操作,请忽略此邮件。", 'aksrt'), $user->user_login, $site_name, $url ); return \AKSRT\Settings\Settings::send_event_email( 'reg_verify_link', $email, $default_subject, $default_content, ['{username}' => $user->user_login, '{site_name}' => $site_name, '{url}' => $url] ); } public static function send_registration_verification_code_email(\WP_User $user, string $email, string $code): bool { $site_name = get_bloginfo('name'); $default_subject = '[' . $site_name . '] ' . __('邮箱验证码', 'aksrt'); $default_content = sprintf( __("您好 %s,\n\n欢迎注册 %s。\n\n你的邮箱验证码为:\n\n【%s】\n\n验证码验证成功后系统将自动为你登录。\n\n如果这不是你的操作,请忽略此邮件。", 'aksrt'), $user->user_login, $site_name, $code ); return \AKSRT\Settings\Settings::send_event_email( 'reg_verify_code', $email, $default_subject, $default_content, ['{username}' => $user->user_login, '{site_name}' => $site_name, '{code}' => $code] ); } public static function send_registration_verified_notice_email(\WP_User $user, string $email): bool { $site_name = get_bloginfo('name'); $default_subject = '[' . $site_name . '] ' . __('邮箱验证成功', 'aksrt'); $default_content = sprintf( __("您好 %s,\n\n你在 %s 的账号邮箱已经验证成功。\n\n如果这不是你的操作,请立即联系站点管理员。", 'aksrt'), $user->user_login, $site_name ); return \AKSRT\Settings\Settings::send_event_email( 'reg_success', $email, $default_subject, $default_content, ['{username}' => $user->user_login, '{site_name}' => $site_name] ); } }