feat(users): add native VIP management

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 18:30:54 +08:00
co-authored by Claude Fable 5
parent 1bd09dcda3
commit 703eedfba1
9 changed files with 570 additions and 1 deletions
+133
View File
@@ -0,0 +1,133 @@
<?php
/**
* Administrator membership mutations.
*
* @package AKSRT
*/
namespace AKSRT\Content;
defined('ABSPATH') || exit;
trait VipServiceMembership {
/** @return array{level:int,expires:int,base_level:int,base_expires:int,expired_level:int} */
public function get_membership_snapshot(int $user_id): array {
return [
'level' => 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<string,mixed> $payload @param array<string,mixed> $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'], 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<string,mixed> $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<string,mixed> $before @param array<string,mixed> $payload @return array{level:int,expires:int}|\WP_Error */
private function membership_target(array $before, string $operation, array $payload): array|\WP_Error {
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()];
}
}