max(0, min(2, (int) get_user_meta($user_id, 'aksrt_vip_level', true))), 'expires' => max(0, (int) get_user_meta($user_id, 'aksrt_vip_expires', true)), 'base_level' => max(0, min(2, (int) get_user_meta($user_id, 'aksrt_vip_base_level', true))), 'base_expires' => max(0, (int) get_user_meta($user_id, 'aksrt_vip_base_expires', true)), 'expired_level' => max(0, min(2, (int) get_user_meta($user_id, 'aksrt_vip_level_expired', true))), ]; } /** @param array $payload @param array $context */ public function mutate_membership(int $user_id, string $operation, array $payload, array $context = []): array|\WP_Error { if ($user_id <= 0 || !get_userdata($user_id)) { return new \WP_Error('aksrt_vip_user_missing', __('用户不存在。', 'aksrt')); } if (!in_array($operation, ['set', 'extend', 'disable', 'restore'], true)) { return new \WP_Error('aksrt_vip_operation_invalid', __('会员操作无效。', 'aksrt')); } $reason = sanitize_text_field((string) ($context['reason'] ?? '')); if ($reason === '') { return new \WP_Error('aksrt_vip_reason_required', __('请填写变更原因。', 'aksrt')); } $before = $this->get_membership_snapshot($user_id); $repository = new VipMembershipEventRepository(); $event_uuid = $repository->begin([ 'batch_uuid' => $context['batch_uuid'] ?? '', 'user_id' => $user_id, 'actor_user_id' => (int) ($context['actor_user_id'] ?? 0), 'operation' => $operation, 'source' => $context['source'] ?? 'admin', 'reason' => $reason, 'request' => $payload, 'before' => $before, ]); if (is_wp_error($event_uuid)) { return $event_uuid; } $target = $this->membership_target($before, $operation, $payload); if (is_wp_error($target)) { $repository->complete($event_uuid, 'failed', $before, $target->get_error_code(), $target->get_error_message()); return $target; } delete_user_meta($user_id, 'aksrt_vip_base_level'); delete_user_meta($user_id, 'aksrt_vip_base_expires'); delete_user_meta($user_id, 'aksrt_vip_level_expired'); update_user_meta($user_id, 'aksrt_vip_level', $target['level']); update_user_meta($user_id, 'aksrt_vip_expires', $target['expires']); $after = $this->get_membership_snapshot($user_id); if ($after['level'] !== $target['level'] || $after['expires'] !== $target['expires'] || $after['base_level'] !== 0 || $after['base_expires'] !== 0 || $after['expired_level'] !== 0) { $this->restore_membership_snapshot($user_id, $before); $after = $this->get_membership_snapshot($user_id); $error = new \WP_Error('aksrt_vip_write_failed', __('会员状态保存失败。', 'aksrt')); $repository->complete($event_uuid, 'failed', $after, $error->get_error_code(), $error->get_error_message()); return $error; } unset($this->vip_member_cache[$user_id], $this->vip_catalog_cache[$user_id]); $repository->complete($event_uuid, 'succeeded', $after); do_action('aksrt_vip_membership_mutated', $user_id, $operation, $before, $after, $context, $event_uuid); return ['event_uuid' => $event_uuid, 'before' => $before, 'after' => $after]; } /** @param array $snapshot */ private function restore_membership_snapshot(int $user_id, array $snapshot): void { foreach ([ 'aksrt_vip_level' => 'level', 'aksrt_vip_expires' => 'expires', 'aksrt_vip_base_level' => 'base_level', 'aksrt_vip_base_expires' => 'base_expires', 'aksrt_vip_level_expired' => 'expired_level', ] as $meta_key => $snapshot_key) { $value = (int) ($snapshot[$snapshot_key] ?? 0); if ($value > 0 || $meta_key === 'aksrt_vip_level' || ($meta_key === 'aksrt_vip_expires' && (int) ($snapshot['level'] ?? 0) > 0)) { update_user_meta($user_id, $meta_key, $value); } else { delete_user_meta($user_id, $meta_key); } } } /** @param array $before @param array $payload @return array{level:int,expires:int}|\WP_Error */ private function membership_target(array $before, string $operation, array $payload): array|\WP_Error { if ($operation === 'restore') { $level = max(0, min(2, (int) $before['base_level'])); $expires = max(0, (int) $before['base_expires']); if ($level <= 0 || ($expires > 0 && $expires <= time())) { return new \WP_Error('aksrt_vip_restore_unavailable', __('没有有效的回退会员可恢复。', 'aksrt')); } return ['level' => $level, 'expires' => $expires]; } if ($operation === 'disable') { return ['level' => 0, 'expires' => 0]; } if ($operation === 'set') { $level = (int) ($payload['level'] ?? 0); if (!in_array($level, [1, 2], true)) { return new \WP_Error('aksrt_vip_level_invalid', __('会员档位无效。', 'aksrt')); } $expires = (int) ($payload['expires'] ?? -1); if ($expires < 0 || ($expires > 0 && $expires < time())) { return new \WP_Error('aksrt_vip_expiry_invalid', __('到期时间无效或已过期。', 'aksrt')); } return ['level' => $level, 'expires' => $expires]; } $amount = (int) ($payload['amount'] ?? 0); $unit = sanitize_key((string) ($payload['unit'] ?? 'day')); if ($before['level'] < 1) { return new \WP_Error('aksrt_vip_extend_unavailable', __('无有效会员,无法延长。', 'aksrt')); } if ($before['expires'] <= 0) { return new \WP_Error('aksrt_vip_extend_permanent', __('永久会员无需延长。', 'aksrt')); } if ($amount < 1 || $amount > 3650 || !in_array($unit, ['day', 'month'], true)) { return new \WP_Error('aksrt_vip_duration_invalid', __('延长时长无效。', 'aksrt')); } $base = max(time(), $before['expires']); $date = (new \DateTimeImmutable('@' . $base))->setTimezone(wp_timezone()); $next = $date->modify('+' . $amount . ' ' . ($unit === 'month' ? 'months' : 'days')); if (!$next) { return new \WP_Error('aksrt_vip_duration_invalid', __('无法计算新的到期时间。', 'aksrt')); } return ['level' => $before['level'], 'expires' => $next->getTimestamp()]; } }