feat(vip): implement layered membership expiry flow
This commit is contained in:
1 parent
ddc762bfbd
commit
80283df6b2
8 files changed
+251
-47
No files matched your search
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
/**
|
||||
* VIP layered membership expiry and recovery service.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Content;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
final class VipMembershipExpiry {
|
||||
|
||||
public const CRON_HOOK = 'aksrt_vip_membership_expiry';
|
||||
private const BATCH_SIZE = 100;
|
||||
|
||||
public static function init(): void {
|
||||
add_action(self::CRON_HOOK, [self::class, 'run_batch']);
|
||||
add_action('init', [self::class, 'maybe_schedule'], 20);
|
||||
}
|
||||
|
||||
public static function maybe_schedule(): void {
|
||||
if (!wp_next_scheduled(self::CRON_HOOK)) {
|
||||
wp_schedule_event(time() + 300, 'hourly', self::CRON_HOOK);
|
||||
}
|
||||
}
|
||||
|
||||
public static function run_batch(): void {
|
||||
global $wpdb;
|
||||
$now = time();
|
||||
$rows = $wpdb->get_col($wpdb->prepare(
|
||||
"SELECT DISTINCT um.user_id FROM {$wpdb->usermeta} um
|
||||
WHERE um.meta_key = 'aksrt_vip_level'
|
||||
AND CAST(um.meta_value AS UNSIGNED) > 0
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM {$wpdb->usermeta} expiry
|
||||
WHERE expiry.user_id = um.user_id
|
||||
AND expiry.meta_key = 'aksrt_vip_expires'
|
||||
AND CAST(expiry.meta_value AS UNSIGNED) > 0
|
||||
AND CAST(expiry.meta_value AS UNSIGNED) <= %d
|
||||
)
|
||||
ORDER BY user_id ASC LIMIT %d",
|
||||
$now,
|
||||
self::BATCH_SIZE
|
||||
));
|
||||
foreach ((array) $rows as $user_id) {
|
||||
self::process_user((int) $user_id, 'cron');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an expired current layer exactly once.
|
||||
*
|
||||
* @return array{changed:bool,before:array,after:array}|\WP_Error
|
||||
*/
|
||||
public static function process_user(int $user_id, string $source = 'resolver', string $reason = ''): array|\WP_Error {
|
||||
if ($user_id <= 0 || !get_userdata($user_id)) {
|
||||
return new \WP_Error('aksrt_vip_user_missing', __('用户不存在。', 'aksrt'));
|
||||
}
|
||||
$before = self::snapshot($user_id);
|
||||
if ($before['level'] <= 0 || $before['expires'] <= 0 || $before['expires'] > time()) {
|
||||
return ['changed' => false, 'before' => $before, 'after' => $before];
|
||||
}
|
||||
|
||||
$repository = new VipMembershipEventRepository();
|
||||
$event_uuid = $repository->begin([
|
||||
'user_id' => $user_id,
|
||||
'actor_user_id' => 0,
|
||||
'operation' => 'expire_recover',
|
||||
'source' => $source,
|
||||
'reason' => $reason !== '' ? $reason : __('会员当前层到期处理', 'aksrt'),
|
||||
'request' => ['now' => time()],
|
||||
'before' => $before,
|
||||
]);
|
||||
if (is_wp_error($event_uuid)) {
|
||||
return $event_uuid;
|
||||
}
|
||||
|
||||
$fallback_valid = $before['base_level'] > 0
|
||||
&& ($before['base_expires'] === 0 || $before['base_expires'] > time());
|
||||
if ($fallback_valid) {
|
||||
update_user_meta($user_id, 'aksrt_vip_level', $before['base_level']);
|
||||
update_user_meta($user_id, 'aksrt_vip_expires', $before['base_expires']);
|
||||
} else {
|
||||
update_user_meta($user_id, 'aksrt_vip_level_expired', max($before['expired_level'], $before['level']));
|
||||
update_user_meta($user_id, 'aksrt_vip_level', 0);
|
||||
update_user_meta($user_id, 'aksrt_vip_expires', 0);
|
||||
}
|
||||
delete_user_meta($user_id, 'aksrt_vip_base_level');
|
||||
delete_user_meta($user_id, 'aksrt_vip_base_expires');
|
||||
|
||||
$after = self::snapshot($user_id);
|
||||
$expected_level = $fallback_valid ? $before['base_level'] : 0;
|
||||
$expected_expires = $fallback_valid ? $before['base_expires'] : 0;
|
||||
if ($after['level'] !== $expected_level || $after['expires'] !== $expected_expires || $after['base_level'] !== 0 || $after['base_expires'] !== 0) {
|
||||
self::restore($user_id, $before);
|
||||
$after = self::snapshot($user_id);
|
||||
$error = new \WP_Error('aksrt_vip_expiry_write_failed', __('会员到期状态保存失败。', 'aksrt'));
|
||||
$repository->complete($event_uuid, 'failed', $after, $error->get_error_code(), $error->get_error_message());
|
||||
return $error;
|
||||
}
|
||||
|
||||
$repository->complete($event_uuid, 'succeeded', $after);
|
||||
do_action('aksrt_vip_membership_expired', $user_id, $before, $after, $source, $event_uuid);
|
||||
return ['changed' => true, 'before' => $before, 'after' => $after];
|
||||
}
|
||||
|
||||
/** @return array{level:int,expires:int,base_level:int,base_expires:int,expired_level:int} */
|
||||
public static function 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,int> $snapshot */
|
||||
private static function restore(int $user_id, array $snapshot): void {
|
||||
foreach (['level' => 'aksrt_vip_level', 'expires' => 'aksrt_vip_expires', 'base_level' => 'aksrt_vip_base_level', 'base_expires' => 'aksrt_vip_base_expires', 'expired_level' => 'aksrt_vip_level_expired'] as $key => $meta_key) {
|
||||
$value = (int) ($snapshot[$key] ?? 0);
|
||||
if ($value > 0 || in_array($key, ['level', 'expires'], true)) {
|
||||
update_user_meta($user_id, $meta_key, $value);
|
||||
} else {
|
||||
delete_user_meta($user_id, $meta_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user