get_param('page') ?: 1)); $per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20))); $keyword = trim(sanitize_text_field((string) $request->get_param('keyword'))); $status = sanitize_key((string) $request->get_param('status')); $source = sanitize_key((string) $request->get_param('source')); $date_from = sanitize_text_field((string) $request->get_param('date_from')); $date_to = sanitize_text_field((string) $request->get_param('date_to')); $usermeta_table = $wpdb->usermeta; $invite_table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('invite') : ''; $invite_rows = []; if ($invite_table) { $records = $wpdb->get_results("SELECT * FROM {$invite_table}", ARRAY_A); foreach ((array) $records as $record) { $invite_rows[(string) $record['invite_code']] = $record; } } $items = []; $user_rows = $wpdb->get_results( "SELECT user_id, meta_value AS invite_code FROM {$usermeta_table} WHERE meta_key = 'aksrt_invite_code' ORDER BY umeta_id DESC", ARRAY_A ); foreach ((array) $user_rows as $row) { $user_id = (int) $row['user_id']; $user = get_userdata($user_id); $code = sanitize_key((string) $row['invite_code']); $record = $invite_rows[$code] ?? null; $item = [ 'row_key' => 'user:' . $user_id, 'source_type' => 'user', 'source_label' => __('用户邀请码', 'aksrt'), 'user' => $this->build_admin_user_summary($user_id, $user), 'invite_code' => $code, 'status' => $record ? 'used' : 'active', 'status_label' => $record ? __('已使用', 'aksrt') : __('可用', 'aksrt'), 'created_at' => $user ? (string) $user->user_registered : '', 'used_at' => $record['created_at'] ?? '', 'note' => '', 'batch_id' => '', 'created_by' => null, 'registration_status' => $record['status'] ?? '', 'invitee' => $this->build_admin_user_summary((int) ($record['invitee_id'] ?? 0)), 'can_bulk' => false, 'actions' => ['disable'], ]; if ($this->invite_item_matches_filters($item, $keyword, $status, $source, $date_from, $date_to)) { $items[] = $item; } } foreach ($this->load_standalone_invite_codes() as $entry) { $code = sanitize_key((string) ($entry['code'] ?? '')); $record = $invite_rows[$code] ?? null; $used_by = (int) ($entry['used_by'] ?? 0); $disabled = !empty($entry['disabled']); $used = !empty($entry['used']) || $used_by > 0 || !empty($record['invitee_id']); $status_key = $disabled ? 'disabled' : ($used ? 'used' : 'active'); $status_map = [ 'active' => __('可用', 'aksrt'), 'used' => __('已使用', 'aksrt'), 'disabled' => __('已禁用', 'aksrt'), ]; $invitee_id = $used_by > 0 ? $used_by : (int) ($record['invitee_id'] ?? 0); $item = [ 'row_key' => 'standalone:' . $code, 'source_type' => 'standalone', 'source_label' => __('独立邀请码', 'aksrt'), 'user' => null, 'invite_code' => $code, 'status' => $status_key, 'status_label' => $status_map[$status_key], 'created_at' => (string) ($entry['created_at'] ?? ''), 'used_at' => (string) ($entry['used_at'] ?? ($record['created_at'] ?? '')), 'note' => (string) ($entry['note'] ?? ''), 'batch_id' => (string) ($entry['batch_id'] ?? ''), 'created_by' => $this->build_admin_user_summary((int) ($entry['created_by'] ?? 0)), 'registration_status' => $record['status'] ?? '', 'invitee' => $this->build_admin_user_summary($invitee_id), 'can_bulk' => true, 'actions' => ['disable', 'delete'], ]; if ($this->invite_item_matches_filters($item, $keyword, $status, $source, $date_from, $date_to)) { $items[] = $item; } } usort($items, static function ($left, $right) { return strcmp((string) ($right['created_at'] ?? ''), (string) ($left['created_at'] ?? '')); }); $total = count($items); $offset = ($page - 1) * $per_page; $paged = array_slice($items, $offset, $per_page); $stats = $this->build_invite_operation_stats($items, $invite_rows); return $this->success([ 'data' => [ 'items' => array_values($paged), 'total' => $total, 'page' => $page, 'per_page' => $per_page, 'total_pages' => max(1, (int) ceil(max(1, $total) / $per_page)), 'stats' => $stats, 'options' => [ 'statuses' => [ ['label' => __('全部状态', 'aksrt'), 'value' => ''], ['label' => __('可用', 'aksrt'), 'value' => 'active'], ['label' => __('已使用', 'aksrt'), 'value' => 'used'], ['label' => __('已禁用', 'aksrt'), 'value' => 'disabled'], ], 'sources' => [ ['label' => __('全部来源', 'aksrt'), 'value' => ''], ['label' => __('用户邀请码', 'aksrt'), 'value' => 'user'], ['label' => __('独立邀请码', 'aksrt'), 'value' => 'standalone'], ], ], ], ]); } /** * POST /invite/codes — 批量生成邀请码(管理后台) * Body: { count: int, length: int, prefix: string } * 为指定用户或批量创建邀请码。 */ public function generate_invite_codes(\WP_REST_Request $request): \WP_REST_Response { if (!class_exists('\AKSRT\Credits\Invite')) { return $this->error(__('邀请系统未启用', 'aksrt'), 500); } $params = $this->get_json_params($request); $count = max(1, min(100, (int) ($params['count'] ?? 1))); $length = max(4, min(12, (int) ($params['length'] ?? 6))); $prefix = sanitize_key((string) ($params['prefix'] ?? '')); $note = sanitize_textarea_field((string) ($params['note'] ?? '')); $batch_id = sanitize_text_field((string) ($params['batch_id'] ?? '')); if ($batch_id === '') { $batch_id = 'INV-' . gmdate('YmdHis') . '-' . wp_generate_password(4, false, false); } $generated = []; $codes = $this->load_standalone_invite_codes(); $alphabet = 'abcdefghjkmnpqrstuvwxyz23456789'; for ($i = 0; $i < $count; $i++) { $code = ''; for ($attempt = 0; $attempt < 12; $attempt++) { $candidate = $prefix; for ($j = 0; $j < $length; $j++) { $candidate .= $alphabet[random_int(0, strlen($alphabet) - 1)]; } $candidate = sanitize_key($candidate); if (!$this->invite_code_exists_globally($candidate, $codes)) { $code = $candidate; break; } } if ($code === '') { continue; } $codes[] = [ 'code' => $code, 'created_at' => current_time('mysql'), 'used' => false, 'used_by' => 0, 'used_at' => '', 'disabled' => false, 'note' => $note, 'batch_id' => $batch_id, 'created_by' => get_current_user_id(), ]; $generated[] = $code; } $this->save_standalone_invite_codes($codes); return $this->success([ 'message' => sprintf(__('已生成 %d 个邀请码', 'aksrt'), count($generated)), 'data' => [ 'codes' => $generated, 'batch_id' => $batch_id, ], ]); } /** * POST /invite/codes/{id}/disable — 禁用邀请码 */ public function disable_invite_code(\WP_REST_Request $request): \WP_REST_Response { $user_id = (int) $request->get_param('id'); if ($user_id <= 0) { return $this->error(__('参数无效', 'aksrt')); } $removed = $this->remove_user_invite_code($user_id); if (!$removed) { return $this->error(__('邀请码不存在或已禁用', 'aksrt')); } return $this->success(['message' => __('邀请码已禁用', 'aksrt')]); } public function bulk_update_invite_codes(\WP_REST_Request $request): \WP_REST_Response { $params = $this->get_json_params($request); $action = sanitize_key((string) ($params['action'] ?? '')); $targets = isset($params['targets']) && is_array($params['targets']) ? $params['targets'] : []; if (!in_array($action, ['disable', 'delete'], true) || empty($targets)) { return $this->error(__('批量操作参数无效', 'aksrt')); } $codes = $this->load_standalone_invite_codes(); $processed = 0; foreach ($targets as $target) { $source_type = sanitize_key((string) ($target['source_type'] ?? '')); if ($source_type === 'user') { $user_id = (int) ($target['user_id'] ?? 0); if ($action === 'disable' && $this->remove_user_invite_code($user_id)) { $processed++; } continue; } if ($source_type !== 'standalone') { continue; } $code = sanitize_key((string) ($target['code'] ?? $target['invite_code'] ?? '')); foreach ($codes as $index => $entry) { $entry_code = sanitize_key((string) ($entry['code'] ?? '')); if ($entry_code !== $code) { continue; } if ($action === 'delete' && empty($entry['used']) && empty($entry['used_by'])) { unset($codes[$index]); } else { $entry['disabled'] = true; $codes[$index] = $entry; } $processed++; break; } } $this->save_standalone_invite_codes($codes); return $this->success([ 'message' => sprintf(__('已处理 %d 条邀请码记录', 'aksrt'), $processed), ]); } /** * GET /invite/stats — 邀请系统统计 */ public function get_invite_stats(): \WP_REST_Response { global $wpdb; $items = []; $user_records = $wpdb->get_results( "SELECT user_id, meta_value AS invite_code FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code'", ARRAY_A ); foreach ((array) $user_records as $row) { $items[] = [ 'source_type' => 'user', 'status' => 'active', ]; } foreach ($this->load_standalone_invite_codes() as $entry) { $used = !empty($entry['used']) || !empty($entry['used_by']); $disabled = !empty($entry['disabled']); $items[] = [ 'source_type' => 'standalone', 'status' => $disabled ? 'disabled' : ($used ? 'used' : 'active'), ]; } $stats = $this->build_invite_operation_stats($items); return $this->success(['data' => $stats]); } private function load_standalone_invite_codes(): array { $codes = get_option('aksrt_standalone_invite_codes', []); return is_array($codes) ? array_values($codes) : []; } private function save_standalone_invite_codes(array $codes): void { update_option('aksrt_standalone_invite_codes', array_values($codes), false); } private function invite_code_exists_globally(string $candidate, array $standalone_codes = []): bool { if ($candidate === '') { return true; } global $wpdb; $existing_user_code = $wpdb->get_var($wpdb->prepare( "SELECT umeta_id FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code' AND meta_value = %s LIMIT 1", $candidate )); if ($existing_user_code) { return true; } foreach ($standalone_codes as $entry) { if (sanitize_key((string) ($entry['code'] ?? '')) === $candidate) { return true; } } $invite_table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('invite') : ''; if ($invite_table) { $existing_invite = $wpdb->get_var($wpdb->prepare( "SELECT id FROM {$invite_table} WHERE invite_code = %s LIMIT 1", $candidate )); if ($existing_invite) { return true; } } return false; } private function invite_item_matches_filters(array $item, string $keyword, string $status, string $source, string $date_from, string $date_to): bool { if ($status !== '' && ($item['status'] ?? '') !== $status) { return false; } if ($source !== '' && ($item['source_type'] ?? '') !== $source) { return false; } $created_at = (string) ($item['created_at'] ?? ''); if ($date_from !== '' && $created_at !== '' && strcmp($created_at, $date_from . ' 00:00:00') < 0) { return false; } if ($date_to !== '' && $created_at !== '' && strcmp($created_at, $date_to . ' 23:59:59') > 0) { return false; } if ($keyword === '') { return true; } $haystack = implode(' ', array_filter([ (string) ($item['invite_code'] ?? ''), (string) ($item['status_label'] ?? ''), (string) ($item['note'] ?? ''), (string) ($item['batch_id'] ?? ''), (string) ($item['user']['display_name'] ?? ''), (string) ($item['invitee']['display_name'] ?? ''), (string) ($item['invitee']['email'] ?? ''), ])); return mb_stripos($haystack, $keyword) !== false; } private function build_invite_operation_stats(array $items, array $invite_rows = []): array { $stats = [ 'total_codes' => count($items), 'active_codes' => 0, 'used_codes' => 0, 'disabled_codes' => 0, 'user_codes' => 0, 'standalone_codes' => 0, 'total_invites' => count($invite_rows), 'total_rewarded' => 0, ]; foreach ($items as $item) { $status = $item['status'] ?? ''; if ($status === 'active') { $stats['active_codes']++; } elseif ($status === 'used') { $stats['used_codes']++; } elseif ($status === 'disabled') { $stats['disabled_codes']++; } if (($item['source_type'] ?? '') === 'user') { $stats['user_codes']++; } elseif (($item['source_type'] ?? '') === 'standalone') { $stats['standalone_codes']++; } } foreach ($invite_rows as $row) { if (($row['status'] ?? '') === 'rewarded') { $stats['total_rewarded']++; } } return $stats; } private function remove_user_invite_code(int $user_id): bool { $user_id = (int) $user_id; if ($user_id <= 0) { return false; } $code = get_user_meta($user_id, 'aksrt_invite_code', true); if (!$code) { return false; } delete_user_meta($user_id, 'aksrt_invite_code'); $map = get_option('aksrt_invite_code_map', []); if (is_array($map) && isset($map[$code])) { unset($map[$code]); update_option('aksrt_invite_code_map', $map, false); } return true; } }