feat(users): add native VIP management
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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()];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user