ensure_cap(); $tab = $this->vip_tab(); $page = max(1, absint($_GET['paged'] ?? 1)); echo '

' . esc_html__('用户会员', 'aksrt') . '

'; $this->flash(); $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, 20); $this->render_vip_filters($filters); $this->render_vip_bulk_form($filters); $this->render_vip_table($result['items'], $filters); $this->pagination($result['pages'], $page); } echo '
'; } 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; 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')); is_wp_error($result) ? $failed++ : $ok++; } $message = $ok > 0 && ($failed > 0 || $skipped > 0) ? 'vip_partial' : ($ok > 0 ? 'vip_updated' : 'vip_failed'); $this->redirect_vip_result($message, [ '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_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 : ''; } /** @return array{items:array>,pages:int} */ private function query_vip_users(array $filters, int $page, int $per_page): 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(); $level = "CASE WHEN {$raw_level} > 0 AND {$raw_expires} > 0 AND {$raw_expires} < {$now} AND {$base_level} > 0 THEN {$base_level} ELSE {$raw_level} END"; $expires = "CASE WHEN {$raw_level} > 0 AND {$raw_expires} > 0 AND {$raw_expires} < {$now} AND {$base_level} > 0 THEN {$base_expires} ELSE {$raw_expires} END"; $is_expired = "({$raw_level} > 0 AND {$raw_expires} > 0 AND {$raw_expires} < {$now} AND {$base_level} = 0) OR ({$raw_level} = 0 AND {$expired_level} > 0)"; $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[] = $now; } elseif ($filters['state'] === 'permanent') { $where[] = "{$level} > 0 AND {$expires} = 0"; } elseif ($filters['state'] === 'expired') { $where[] = $is_expired; } elseif ($filters['state'] === 'none') { $where[] = "{$level} = 0 AND NOT {$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 LEFT JOIN {$meta} ON m.user_id = u.ID 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, {$raw_level} raw_level, {$raw_expires} raw_expires, {$base_level} base_level, {$base_expires} base_expires, {$expired_level} expired_level, {$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))]; } private function render_vip_tabs(string $tab): void { $base = admin_url('users.php?page=aksrt_user_vip'); echo ''; } private function render_vip_filters(array $filters): void { $names = $this->vip_level_names(); echo '
'; echo ''; submit_button(__('筛选', 'aksrt'), 'secondary', '', false); echo '
'; } private function render_vip_bulk_form(array $filters): void { $names = $this->vip_level_names(); echo '
'; echo ''; wp_nonce_field('aksrt_users_mutate_vip'); $this->render_vip_return_fields($filters); echo '' . esc_html__('批量操作', 'aksrt') . ' '; $this->render_vip_operation_fields($names); echo ' '; submit_button(__('应用到选中用户', 'aksrt'), 'primary', '', false); echo '
'; } private function render_vip_table(array $items, array $filters): void { echo ''; if ($items === []) { echo ''; } foreach ($items as $row) { $this->render_vip_row($row, $filters); } echo '
UID' . esc_html__('用户', 'aksrt') . '' . esc_html__('当前会员', 'aksrt') . '' . esc_html__('到期时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
' . esc_html__('暂无用户', 'aksrt') . '
'; } private function render_vip_row(array $row, array $filters): void { $id = (int) $row['ID']; $names = $this->vip_level_names(); $level = (int) $row['level']; $expires = (int) $row['expires']; $label = $level > 0 ? ($names[$level] ?? ('VIP' . $level)) : __('普通用户', 'aksrt'); $expiry = !empty($row['is_expired']) ? __('已过期', 'aksrt') : ($level > 0 ? ($expires === 0 ? __('永久有效', 'aksrt') : wp_date('Y-m-d H:i', $expires, wp_timezone())) : '—'); if ((int) $row['raw_level'] !== $level || (int) $row['raw_expires'] !== $expires) { $label .= ' (' . __('基础档位恢复后', 'aksrt') . ')'; } echo '' . $id . '' . esc_html((string) $row['display_name']) . '
@' . esc_html((string) $row['user_login']) . '
' . esc_html($label) . '' . esc_html($expiry) . '
' . esc_html__('调整', 'aksrt') . '
'; wp_nonce_field('aksrt_users_mutate_vip'); $this->render_vip_return_fields($filters); $this->render_vip_operation_fields($names); echo '

'; submit_button(__('保存', 'aksrt'), 'secondary', '', false); echo '
'; } private function render_vip_operation_fields(array $names): void { echo ' '; echo ' '; echo ' '; echo ''; echo ''; } /** @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') { return []; } if ($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]; } private function render_vip_history(int $page): void { $result = (new \AKSRT\Content\VipMembershipEventRepository())->query($page, 20); echo ''; if ($result['items'] === []) { echo ''; } foreach ($result['items'] as $event) { $user = (string) ($event['display_name'] ?: $event['user_login']); echo ''; } echo '
' . esc_html__('时间', 'aksrt') . '' . esc_html__('用户', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '' . esc_html__('操作人', 'aksrt') . '' . esc_html__('原因', 'aksrt') . '' . esc_html__('结果', 'aksrt') . '
' . esc_html__('暂无变更记录', 'aksrt') . '
' . esc_html((string) $event['created_at']) . '' . esc_html($user) . ' (#' . (int) $event['user_id'] . ')' . esc_html((string) $event['operation']) . '' . esc_html((string) ($event['actor_name'] ?: '—')) . '' . esc_html((string) $event['reason']) . '' . esc_html((string) $event['status']) . '
'; $this->pagination($result['pages'], $page); } /** @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 (['user_id', 'level', 'state', 'date_from', 'date_to', 'orderby', 'order'] as $key) { echo ''; } } private function redirect_vip_result(string $message, array $extra = []): void { $filters = []; foreach (['user_id', 'level', 'state', 'date_from', 'date_to', 'orderby', 'order'] as $key) { if (isset($_POST['return_' . $key])) { $filters[$key] = sanitize_text_field((string) wp_unslash($_POST['return_' . $key])); } } $this->redirect('aksrt_user_vip', $message, array_merge($filters, $extra)); } }