ensure_cap(); $tab = $this->vip_tab(); $page = max(1, absint($_GET['paged'] ?? 1)); NativeUi::page_open('aksrt-vip'); NativeUi::header( __('用户会员', 'aksrt'), __('查看会员档位与有效期,单人或批量调整会员;每次变更都会记录原因与结果。', 'aksrt'), 'dashicons-awards', [[ 'label' => __('会员配置', 'aksrt'), 'url' => admin_url('admin.php?page=aksrt-settings#/payment/vip'), 'icon' => 'dashicons-admin-generic', ]] ); $this->flash(); $this->render_vip_stats(); $this->render_vip_tabs($tab); if ($tab === 'history') { $this->render_vip_history($page); } else { $filters = $this->vip_filters(); $result = $this->query_vip_users($filters, $page, self::VIP_PER_PAGE); $this->render_vip_filters($filters); $this->render_vip_table($result['items'], $filters, $result['pages'], $page); $this->render_vip_dialog($filters); } NativeUi::page_close(); } public function handle_mutate_vip(): void { $this->ensure_cap(); check_admin_referer('aksrt_users_mutate_vip'); $ids = isset($_POST['user_ids']) ? wp_unslash($_POST['user_ids']) : []; $ids = is_array($ids) ? array_values(array_unique(array_filter(array_map('absint', $ids)))) : []; if ($ids === [] || count($ids) > 100) { $this->redirect_vip_result('vip_failed', ['aksrt_vip_error' => __('请选择 1 至 100 名用户。', 'aksrt')]); } $operation = sanitize_key((string) wp_unslash($_POST['vip_operation'] ?? '')); $reason = sanitize_text_field((string) wp_unslash($_POST['reason'] ?? '')); $payload = $this->vip_mutation_payload($operation); if (is_wp_error($payload)) { $this->redirect_vip_result('vip_failed', ['aksrt_vip_error' => $payload->get_error_message()]); } $batch = count($ids) > 1 ? wp_generate_uuid4() : ''; $ok = $failed = $skipped = 0; $last_error = ''; foreach ($ids as $user_id) { if (!get_userdata($user_id) || !current_user_can('edit_user', $user_id)) { $skipped++; continue; } $result = class_exists('\AKSRT\Content\VipService') ? \AKSRT\Content\VipService::get_instance()->mutate_membership($user_id, $operation, $payload, [ 'actor_user_id' => get_current_user_id(), 'source' => 'admin', 'reason' => $reason, 'batch_uuid' => $batch, ]) : new \WP_Error('aksrt_vip_unavailable', __('会员服务不可用。', 'aksrt')); if (is_wp_error($result)) { $failed++; $last_error = $result->get_error_message(); } else { $ok++; } } if ($ok === 0) { $this->redirect_vip_result('vip_failed', $last_error !== '' ? ['aksrt_vip_error' => $last_error] : []); } $this->redirect_vip_result($failed > 0 || $skipped > 0 ? 'vip_partial' : 'vip_updated', [ 'aksrt_vip_ok' => $ok, 'aksrt_vip_failed_count' => $failed, 'aksrt_vip_skipped' => $skipped, ]); } private function vip_tab(): string { return (isset($_GET['tab']) && sanitize_key((string) wp_unslash($_GET['tab'])) === 'history') ? 'history' : 'members'; } /** @return array{user_id:int,level:string,state:string,date_from:string,date_to:string,orderby:string,order:string} */ private function vip_filters(): array { $level = isset($_GET['level']) ? sanitize_key((string) wp_unslash($_GET['level'])) : 'all'; $state = isset($_GET['state']) ? sanitize_key((string) wp_unslash($_GET['state'])) : 'all'; return [ 'user_id' => absint($_GET['user_id'] ?? 0), 'level' => in_array($level, ['all', '0', '1', '2'], true) ? $level : 'all', 'state' => in_array($state, ['all', 'active', 'permanent', 'expired', 'none'], true) ? $state : 'all', 'date_from' => $this->vip_date($_GET['date_from'] ?? ''), 'date_to' => $this->vip_date($_GET['date_to'] ?? ''), 'orderby' => (isset($_GET['orderby']) && sanitize_key((string) wp_unslash($_GET['orderby'])) === 'expires') ? 'expires' : 'id', 'order' => (isset($_GET['order']) && strtoupper((string) wp_unslash($_GET['order'])) === 'ASC') ? 'ASC' : 'DESC', ]; } private function vip_filters_active(array $filters): bool { return $filters['user_id'] > 0 || $filters['level'] !== 'all' || $filters['state'] !== 'all' || $filters['date_from'] !== '' || $filters['date_to'] !== ''; } /** 当前筛选 + 覆盖项组成的列表 URL(只带非默认值)。 */ private function vip_list_url(array $filters, array $override = []): string { $args = []; foreach (array_merge($filters, $override) as $key => $value) { $default = ['user_id' => 0, 'level' => 'all', 'state' => 'all', 'date_from' => '', 'date_to' => '', 'orderby' => 'id', 'order' => 'DESC'][$key] ?? null; if ((string) $value !== (string) $default) { $args[$key] = $value; } } return add_query_arg($args, admin_url('users.php?page=' . self::VIP_PAGE_SLUG)); } private function vip_date($value): string { $value = sanitize_text_field((string) wp_unslash($value)); $date = \DateTimeImmutable::createFromFormat('!Y-m-d', $value, wp_timezone()); return $date && $date->format('Y-m-d') === $value ? $value : ''; } /** * 有效档位/到期时间的 SQL 片段:限时档位过期且存在基础档位时按基础档位计。 * * @return array{join:string,raw_level:string,raw_expires:string,base_level:string,base_expires:string,expired_level:string,level:string,expires:string,is_expired:string,now:int} */ private function vip_sql_parts(): array { global $wpdb; $meta = "(SELECT user_id, MAX(CASE WHEN meta_key = 'aksrt_vip_level' THEN meta_value END) level, MAX(CASE WHEN meta_key = 'aksrt_vip_expires' THEN meta_value END) expires, MAX(CASE WHEN meta_key = 'aksrt_vip_base_level' THEN meta_value END) base_level, MAX(CASE WHEN meta_key = 'aksrt_vip_base_expires' THEN meta_value END) base_expires, MAX(CASE WHEN meta_key = 'aksrt_vip_level_expired' THEN meta_value END) expired_level FROM {$wpdb->usermeta} WHERE meta_key IN ('aksrt_vip_level', 'aksrt_vip_expires', 'aksrt_vip_base_level', 'aksrt_vip_base_expires', 'aksrt_vip_level_expired') GROUP BY user_id) m"; $raw_level = 'CAST(COALESCE(m.level, 0) AS UNSIGNED)'; $raw_expires = 'CAST(COALESCE(m.expires, 0) AS UNSIGNED)'; $base_level = 'CAST(COALESCE(m.base_level, 0) AS UNSIGNED)'; $base_expires = 'CAST(COALESCE(m.base_expires, 0) AS UNSIGNED)'; $expired_level = 'CAST(COALESCE(m.expired_level, 0) AS UNSIGNED)'; $now = time(); $lapsed = "{$raw_level} > 0 AND {$raw_expires} > 0 AND {$raw_expires} < {$now}"; return [ 'join' => "LEFT JOIN {$meta} ON m.user_id = u.ID", 'raw_level' => $raw_level, 'raw_expires' => $raw_expires, 'base_level' => $base_level, 'base_expires' => $base_expires, 'expired_level' => $expired_level, 'level' => "CASE WHEN {$lapsed} AND {$base_level} > 0 THEN {$base_level} ELSE {$raw_level} END", 'expires' => "CASE WHEN {$lapsed} AND {$base_level} > 0 THEN {$base_expires} ELSE {$raw_expires} END", 'is_expired' => "(({$lapsed} AND {$base_level} = 0) OR ({$raw_level} = 0 AND {$expired_level} > 0))", 'now' => $now, ]; } /** 「7 天内到期」按自然日:今天 00:00 至第 7 天 23:59:59,与到期日期筛选一致。 */ private function vip_expiring_window(): array { $today = new \DateTimeImmutable('today', wp_timezone()); $end = $today->modify('+7 days'); return ['from' => $today->format('Y-m-d'), 'to' => $end->format('Y-m-d'), 'until' => $end->setTime(23, 59, 59)->getTimestamp()]; } /** @return array{active:int,permanent:int,expired:int,expiring:int,level_1:int,level_2:int} */ private function query_vip_stats(): array { global $wpdb; $p = $this->vip_sql_parts(); $valid = "{$p['level']} > 0 AND ({$p['expires']} = 0 OR {$p['expires']} >= {$p['now']})"; $row = $wpdb->get_row($wpdb->prepare( "SELECT SUM(CASE WHEN {$p['level']} > 0 AND {$p['expires']} >= {$p['now']} THEN 1 ELSE 0 END) active, SUM(CASE WHEN {$p['level']} > 0 AND {$p['expires']} = 0 THEN 1 ELSE 0 END) permanent, SUM(CASE WHEN {$p['is_expired']} THEN 1 ELSE 0 END) expired, SUM(CASE WHEN {$p['level']} > 0 AND {$p['expires']} >= {$p['now']} AND {$p['expires']} <= %d THEN 1 ELSE 0 END) expiring, SUM(CASE WHEN {$valid} AND {$p['level']} = %d THEN 1 ELSE 0 END) level_1, SUM(CASE WHEN {$valid} AND {$p['level']} = %d THEN 1 ELSE 0 END) level_2 FROM {$wpdb->users} u {$p['join']}", $this->vip_expiring_window()['until'], 1, 2 ), ARRAY_A); $stats = []; foreach (['active', 'permanent', 'expired', 'expiring', 'level_1', 'level_2'] as $key) { $stats[$key] = (int) ($row[$key] ?? 0); } return $stats; } /** @return array{items:array>,pages:int} */ private function query_vip_users(array $filters, int $page, int $per_page): array { global $wpdb; $p = $this->vip_sql_parts(); $level = $p['level']; $expires = $p['expires']; $where = ['1=1']; $params = []; if ($filters['user_id']) { $where[] = 'u.ID = %d'; $params[] = $filters['user_id']; } if ($filters['level'] !== 'all') { $where[] = "{$level} = %d"; $params[] = (int) $filters['level']; } if ($filters['state'] === 'active') { $where[] = "{$level} > 0 AND {$expires} >= %d"; $params[] = $p['now']; } elseif ($filters['state'] === 'permanent') { $where[] = "{$level} > 0 AND {$expires} = 0"; } elseif ($filters['state'] === 'expired') { $where[] = $p['is_expired']; } elseif ($filters['state'] === 'none') { $where[] = "{$level} = 0 AND NOT {$p['is_expired']}"; } foreach (['date_from' => '00:00:00', 'date_to' => '23:59:59'] as $key => $time_of_day) { if ($filters[$key] !== '') { $date = new \DateTimeImmutable($filters[$key] . ' ' . $time_of_day, wp_timezone()); $where[] = "{$expires} " . ($key === 'date_from' ? '>=' : '<=') . ' %d'; $params[] = $date->getTimestamp(); } } $from = "FROM {$wpdb->users} u {$p['join']} WHERE " . implode(' AND ', $where); $count_sql = "SELECT COUNT(*) {$from}"; $total = $params ? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params)) : (int) $wpdb->get_var($count_sql); $sort = $filters['orderby'] === 'expires' ? "CASE WHEN {$expires} = 0 THEN 1 ELSE 0 END {$filters['order']}, {$expires} {$filters['order']}, u.ID DESC" : "u.ID {$filters['order']}"; $query = "SELECT u.ID, u.display_name, u.user_login, {$level} level, {$expires} expires, {$p['raw_level']} raw_level, {$p['raw_expires']} raw_expires, {$p['base_level']} base_level, {$p['base_expires']} base_expires, {$p['expired_level']} expired_level, {$p['is_expired']} is_expired {$from} ORDER BY {$sort} LIMIT %d OFFSET %d"; $items = $wpdb->get_results($wpdb->prepare($query, array_merge($params, [$per_page, ($page - 1) * $per_page])), ARRAY_A); return ['items' => is_array($items) ? $items : [], 'pages' => max(1, (int) ceil(max(1, $total) / $per_page))]; } /** @return array|\WP_Error */ private function vip_mutation_payload(string $operation): array|\WP_Error { if (!in_array($operation, ['set', 'extend', 'disable', 'restore'], true)) { return new \WP_Error('aksrt_vip_operation_invalid', __('会员操作无效。', 'aksrt')); } if ($operation === 'disable' || $operation === 'restore') { return []; } if ($operation === 'extend') { return [ 'amount' => absint($_POST['amount'] ?? 0), 'unit' => sanitize_key((string) wp_unslash($_POST['unit'] ?? 'day')), ]; } if (!empty($_POST['permanent'])) { return ['level' => absint($_POST['level'] ?? 0), 'expires' => 0]; } $date = $this->vip_date($_POST['expires_date'] ?? ''); if ($date === '') { return new \WP_Error('aksrt_vip_expiry_required', __('请设置永久会员或有效的到期日期。', 'aksrt')); } $expires = (new \DateTimeImmutable($date . ' 23:59:59', wp_timezone()))->getTimestamp(); return ['level' => absint($_POST['level'] ?? 0), 'expires' => $expires]; } /** @return array */ private function vip_level_names(): array { $levels = class_exists('\AKSRT\Settings\Settings') ? (array) (\AKSRT\Settings\Settings::get_payment_vip_settings()['levels'] ?? []) : []; return [ 1 => (string) ($levels[1]['name'] ?? __('一级会员', 'aksrt')), 2 => (string) ($levels[2]['name'] ?? __('二级会员', 'aksrt')), ]; } private function render_vip_return_fields(array $filters): void { foreach (self::VIP_FILTER_KEYS as $key) { echo ''; } } private function redirect_vip_result(string $message, array $extra = []): void { $filters = []; foreach (self::VIP_FILTER_KEYS as $key) { if (isset($_POST['return_' . $key])) { $filters[$key] = sanitize_text_field((string) wp_unslash($_POST['return_' . $key])); } } $this->redirect(self::VIP_PAGE_SLUG, $message, array_merge($filters, $extra)); } }