From d4946ee5547576eeead5a67506f0038c3aeea214 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 30 Jul 2026 10:21:48 +0800 Subject: [PATCH] fix(user): close level parity gaps --- assets/css/routes/user-center/level.css | 10 ++++ .../user-center-renderers/profile-level.php | 26 ++++++++- src/AKSRT/Admin/Schema/SchemaUsers.php | 26 ++++++++- src/AKSRT/Credits/Invite.php | 36 ++++++++---- src/AKSRT/Credits/Signin.php | 2 +- src/AKSRT/Shop/Part3.php | 5 ++ src/AKSRT/User/Level.php | 56 +++++++++++++------ src/AKSRT/User/LevelAdmin.php | 13 ++++- 8 files changed, 136 insertions(+), 38 deletions(-) diff --git a/assets/css/routes/user-center/level.css b/assets/css/routes/user-center/level.css index 0750e8e8..f22b45f5 100644 --- a/assets/css/routes/user-center/level.css +++ b/assets/css/routes/user-center/level.css @@ -71,6 +71,15 @@ .uc-level-road__item strong { font-size: 12px; } .uc-level-road__item small { color: var(--muted-2-color, #8b9198); font-size: 10px; } +.uc-level-methods { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 10px; } +.uc-level-methods__item { display: grid; grid-template-columns: 34px 1fr auto; align-items: center; gap: 10px; padding: 11px; border: 1px solid var(--color-line-soft, rgba(0,0,0,.06)); border-radius: 9px; } +.uc-level-methods__item > span { display: grid; place-items: center; width: 32px; height: 32px; border-radius: 9px; background: rgba(245,158,11,.12); color: #d97706; } +.uc-level-methods__item strong, .uc-level-methods__item small { display: block; } +.uc-level-methods__item strong { font-size: 12px; } +.uc-level-methods__item small { margin-top: 2px; color: var(--muted-2-color, #8b9198); font-size: 10px; } +.uc-level-methods__item b { color: #7c3aed; } +.uc-level-ledger__row time + small { display: block; margin-top: 2px; color: var(--muted-2-color, #8b9198); font-size: 10px; } + .uc-level-history-grid { display: grid; grid-template-columns: minmax(0,1.35fr) minmax(240px,.65fr); gap: 16px; } .uc-level-ledger, .uc-level-dates { display: grid; } .uc-level-ledger__row { display: grid; grid-template-columns: 36px 1fr auto; align-items: center; gap: 10px; padding: 11px 0; border-bottom: 1px solid var(--color-line-soft, rgba(0,0,0,.05)); } @@ -87,5 +96,6 @@ @media (max-width: 767px) { .uc-level-hero__stats { grid-template-columns: 1fr; } .uc-level-history-grid { grid-template-columns: 1fr; } + .uc-level-methods { grid-template-columns: 1fr; } .uc-level-road { grid-template-columns: repeat(2, minmax(0,1fr)); } } diff --git a/inc/helpers/user-center-renderers/profile-level.php b/inc/helpers/user-center-renderers/profile-level.php index d7a2abbf..03867b2b 100644 --- a/inc/helpers/user-center-renderers/profile-level.php +++ b/inc/helpers/user-center-renderers/profile-level.php @@ -72,6 +72,30 @@ $day_max = \AKSRT\User\Level::day_max(); +
+

+
+ $default_value) : + if ($key === 'day_max') continue; + $value = in_array($key, ['checkin', 'shop_gift'], true) ? 0 : ($key === 'invitation' ? (int) $default_value : \AKSRT\User\Level::task_points($key)); + if ($value <= 0 && !in_array($key, ['checkin', 'shop_gift'], true)) continue; + ?> +
+ +
+ 0 ? '+' . esc_html((string) $value) : esc_html__('动态', 'aksrt'); ?> +
+ +
+
+

@@ -83,7 +107,7 @@ $day_max = \AKSRT\User\Level::day_max(); ?>
-
+
+
diff --git a/src/AKSRT/Admin/Schema/SchemaUsers.php b/src/AKSRT/Admin/Schema/SchemaUsers.php index 61801b76..686b3eee 100644 --- a/src/AKSRT/Admin/Schema/SchemaUsers.php +++ b/src/AKSRT/Admin/Schema/SchemaUsers.php @@ -9,6 +9,27 @@ defined('ABSPATH') || exit; class SchemaUsers { + private static function default_level_options(): array { + $options = []; + for ($level = 1; $level <= 10; $level++) { + $options['name_' . $level] = 'LV' . $level; + $options['icon_img_' . $level] = ''; + $options['upgrade_integral_' . $level] = $level === 1 ? 0 : ($level - 1) * 500 * $level; + } + return $options; + } + + private static function default_integral_options(): array { + return [ + 'day_max' => 100, 'sign_up' => 20, 'sign_in' => 5, 'followed' => 5, + 'post_new' => 5, 'post_like' => 1, 'post_favorite' => 2, + 'comment_new' => 2, 'comment_like' => 1, 'bbs_posts_new' => 3, + 'bbs_score_extra' => 1, 'bbs_essence' => 2, 'bbs_posts_hot' => 2, + 'bbs_plate_new' => 2, 'bbs_plate_hot' => 2, 'bbs_adopt' => 2, + 'bbs_comment_hot' => 2, + ]; + } + public static function build(): array { return [ 'domain' => 'users', @@ -576,7 +597,6 @@ class SchemaUsers { ['key' => 'user_privacy_comments', 'label' => '隐藏评论', 'type' => 'switch', 'default' => false], ['key' => 'user_privacy_favorites', 'label' => '隐藏收藏', 'type' => 'switch', 'default' => false], ['key' => 'user_privacy_credits', 'label' => '隐藏积分', 'type' => 'switch', 'default' => false], - ['key' => 'user_privacy_level', 'label' => '隐藏等级', 'type' => 'switch', 'default' => false], ['key' => 'user_privacy_badge', 'label' => '隐藏徽章', 'type' => 'switch', 'default' => false], ['key' => 'user_privacy_online', 'label' => '隐藏在线状态', 'type' => 'switch', 'default' => false], ['key' => 'user_privacy_last_active', 'label' => '隐藏最后活跃', 'type' => 'switch', 'default' => false], @@ -621,10 +641,10 @@ class SchemaUsers { ['key' => 'user_level_notify', 'label' => '升级通知', 'type' => 'switch', 'default' => true], ]], ['key' => 'level_opt', 'title' => '升级阈值', 'fields' => [ - ['key' => 'user_level_opt', 'label' => '等级参数', 'type' => 'json', 'sanitize_type' => 'array', 'default' => [], 'help' => 'name_N / icon_img_N / upgrade_integral_N'], + ['key' => 'user_level_opt', 'label' => '等级参数', 'type' => 'json', 'sanitize_type' => 'array', 'default' => self::default_level_options(), 'help' => 'name_N / icon_img_N / upgrade_integral_N'], ]], ['key' => 'integral', 'title' => '经验值得分', 'fields' => [ - ['key' => 'user_integral_opt', 'label' => '经验值配置', 'type' => 'json', 'sanitize_type' => 'array', 'default' => [], 'help' => 'day_max + 行为得分键'], + ['key' => 'user_integral_opt', 'label' => '经验值配置', 'type' => 'json', 'sanitize_type' => 'array', 'default' => self::default_integral_options(), 'help' => 'day_max + 行为得分键'], ]], ], ], diff --git a/src/AKSRT/Credits/Invite.php b/src/AKSRT/Credits/Invite.php index 9fe02d91..2141f6cd 100644 --- a/src/AKSRT/Credits/Invite.php +++ b/src/AKSRT/Credits/Invite.php @@ -190,14 +190,27 @@ class Invite { private function reward_pair(int $inviter_id, int $invitee_id): void { if (!class_exists('AKSRT\\Credits\\Credits') || !class_exists('\AKSRT\Settings\Settings')) return; - $invite_cfg = \AKSRT\Settings\Settings::get_invite_reward_settings(); - if (empty($invite_cfg['enabled'])) return; + global $wpdb; + $table = \AKSRT\Core\DBMigrator::table('invite'); + if (!$table) return; + $claimed = $wpdb->update( + $table, + ['status' => 'rewarding'], + ['inviter_id' => $inviter_id, 'invitee_id' => $invitee_id, 'status' => 'verified'], + ['%s'], + ['%d', '%d', '%s'] + ); + if ($claimed !== 1) return; - // 反作弊:同 IP 当日上限 - if (!$this->ip_daily_check($invite_cfg)) return; + $invite_cfg = \AKSRT\Settings\Settings::get_invite_reward_settings(); + if (empty($invite_cfg['enabled']) || !$this->ip_daily_check($invite_cfg)) { + $wpdb->update($table, ['status' => 'verified'], ['inviter_id' => $inviter_id, 'invitee_id' => $invitee_id], ['%s'], ['%d', '%d']); + return; + } $inviter_amount = (int) ($invite_cfg['inviter_amount'] ?? 0); $invitee_amount = (int) ($invite_cfg['invitee_amount'] ?? 0); + $level_integral = max(0, (int) ($invite_cfg['invit_reward_level'] ?? 0)); if ($inviter_amount > 0) { Credits::add($inviter_id, $inviter_amount, 'invite', ['type' => 'user', 'id' => $invitee_id]); @@ -205,12 +218,12 @@ class Invite { if ($invitee_amount > 0) { Credits::add($invitee_id, $invitee_amount, 'invitee', ['type' => 'user', 'id' => $inviter_id]); } + if ($level_integral > 0 && class_exists('\AKSRT\User\Level')) { + \AKSRT\User\Level::add($inviter_id, 'invitation', $level_integral, true); + } // 标记 rewarded - global $wpdb; - $table = \AKSRT\Core\DBMigrator::table('invite'); - if ($table) { - $wpdb->update( + $wpdb->update( $table, [ 'status' => 'rewarded', @@ -220,10 +233,9 @@ class Invite { 'inviter_id' => $inviter_id, 'invitee_id' => $invitee_id, ], - ['%s', '%s'], - ['%d', '%d'] - ); - } + ['%s', '%s'], + ['%d', '%d'] + ); } /** 同 IP 当日邀请兑现次数限制 */ diff --git a/src/AKSRT/Credits/Signin.php b/src/AKSRT/Credits/Signin.php index ffd5684c..cfb6bff3 100644 --- a/src/AKSRT/Credits/Signin.php +++ b/src/AKSRT/Credits/Signin.php @@ -139,7 +139,7 @@ class Signin { return new \WP_Error('aksrt_signin_dup', __('签到失败', 'aksrt'), ['status' => 409]); } - do_action('aksrt_user_signed_in', $user_id); + do_action('aksrt_user_signed_in', $user_id, $reward); if ($reward > 0) { if (class_exists('AKSRT\\Credits\\PointsFree')) { diff --git a/src/AKSRT/Shop/Part3.php b/src/AKSRT/Shop/Part3.php index cbc0be77..37127959 100644 --- a/src/AKSRT/Shop/Part3.php +++ b/src/AKSRT/Shop/Part3.php @@ -367,6 +367,8 @@ trait Part3 { private function process_gifts(string $order_number, array $order, array $items): void { $user_id = (int) ($order['user_id'] ?? 0); if ($user_id <= 0) return; + $claim_key = '_aksrt_shop_gifts_' . sanitize_key($order_number); + if (!add_user_meta($user_id, $claim_key, 'processing', true)) return; foreach ($items as $item) { $gifts = $item['gift_data'] ?? []; @@ -378,6 +380,8 @@ trait Part3 { if ($gift_type === 'credits' && class_exists('\AKSRT\Credits\Credits') && (int) $gift_value > 0) { \AKSRT\Credits\Credits::add((int) $gift_value, $user_id, 'shop_gift', sprintf(__('订单 %s 赠品', 'aksrt'), $order_number)); + } elseif (in_array($gift_type, ['level_integral', 'experience'], true) && class_exists('\AKSRT\User\Level') && (int) $gift_value > 0) { + \AKSRT\User\Level::add($user_id, 'shop_gift', (int) $gift_value, true); } elseif ($gift_type === 'vip_days' && (int) $gift_value > 0) { $current = (int) get_user_meta($user_id, 'aksrt_vip_expires', true); $base = max($current, time()); @@ -390,5 +394,6 @@ trait Part3 { } } } + update_user_meta($user_id, $claim_key, '1'); } } diff --git a/src/AKSRT/User/Level.php b/src/AKSRT/User/Level.php index 5308479b..6708cf41 100644 --- a/src/AKSRT/User/Level.php +++ b/src/AKSRT/User/Level.php @@ -31,7 +31,7 @@ class Level { public function init(): void { add_action('user_register', [$this, 'on_register'], 20, 1); add_action('wp_login', [$this, 'on_login'], 20, 2); - add_action('aksrt_user_signed_in', [$this, 'on_checkin'], 20, 1); + add_action('aksrt_user_signed_in', [$this, 'on_checkin'], 20, 2); add_action('transition_post_status', [$this, 'on_post_status'], 20, 3); add_action('comment_post', [$this, 'on_comment_post'], 20, 3); add_action('transition_comment_status', [$this, 'on_comment_status'], 20, 3); @@ -101,6 +101,9 @@ class Level { return [ 'sign_up' => __('首次注册', 'aksrt'), 'sign_in' => __('每日登录', 'aksrt'), + 'checkin' => __('每日签到', 'aksrt'), + 'invitation' => __('邀请奖励', 'aksrt'), + 'shop_gift' => __('购物赠送', 'aksrt'), 'followed' => __('被关注', 'aksrt'), 'post_new' => __('发布文章', 'aksrt'), 'post_like' => __('文章获赞', 'aksrt'), @@ -134,10 +137,16 @@ class Level { } public static function get_level(int $user_id): int { - if ($user_id <= 0 || !self::settings()['enabled']) { + $cfg = self::settings(); + if ($user_id <= 0 || !$cfg['enabled']) { return 0; } - return self::resolve_level(self::get_integral($user_id)); + $level = (int) get_user_meta($user_id, self::META_LEVEL, true); + if ($level <= 0) { + $level = self::resolve_level(self::get_integral($user_id), $cfg); + update_user_meta($user_id, self::META_LEVEL, $level); + } + return max(1, min($cfg['max'], $level)); } public static function get_level_name(int $level): string { @@ -194,7 +203,7 @@ class Level { public static function get_progress(int $user_id): array { $cfg = self::settings(); $integral = self::get_integral($user_id); - $level = self::resolve_level($integral, $cfg); + $level = self::get_level($user_id); $current_need = $level <= 1 ? 0 : max(0, (int) ($cfg['level_opt']['upgrade_integral_' . $level] ?? (($level - 1) * 500 * $level))); $is_max = $level >= $cfg['max']; $next_need = $is_max ? $integral : max($current_need + 1, (int) ($cfg['level_opt']['upgrade_integral_' . ($level + 1)] ?? ($level * 500 * ($level + 1)))); @@ -252,6 +261,9 @@ class Level { if ($user_id <= 0 || !$cfg['enabled'] || $key === '' || $key === 'day_max') { return false; } + if (class_exists('\AKSRT\User\Restriction') && \AKSRT\User\Restriction::get_instance()->is_banned($user_id)) { + return false; + } $points = $points > 0 ? $points : self::task_points($key); if ($points <= 0) { return false; @@ -261,11 +273,8 @@ class Level { $date_details = self::get_date_details($user_id); $today_total = max(0, (int) ($date_details[$today] ?? 0)); $day_max = self::day_max(); - if (!$bypass_day_max && $day_max > 0) { - if ($today_total >= $day_max) { - return false; - } - $points = min($points, $day_max - $today_total); + if (!$bypass_day_max && $day_max > 0 && $today_total >= $day_max) { + return false; } if ($points <= 0) { return false; @@ -289,7 +298,11 @@ class Level { update_user_meta($user_id, self::META_DETAIL, array_slice($details, 0, 50)); $old_level = max(1, (int) get_user_meta($user_id, self::META_LEVEL, true)); - $new_level = self::resolve_level($after, $cfg); + $new_level = $old_level; + $next_need = max(0, (int) ($cfg['level_opt']['upgrade_integral_' . ($old_level + 1)] ?? PHP_INT_MAX)); + if ($old_level < $cfg['max'] && $after >= $next_need) { + $new_level = self::resolve_level($after, $cfg); + } update_user_meta($user_id, self::META_LEVEL, $new_level); if ($new_level !== $old_level) { do_action('aksrt_user_level_changed', $user_id, $new_level, $old_level); @@ -368,8 +381,10 @@ class Level { } } - public function on_checkin(int $user_id): void { - $this->on_login('', get_userdata($user_id)); + public function on_checkin(int $user_id, int $reward = 0): void { + if ($reward > 0) { + self::add($user_id, 'checkin', $reward); + } } public function on_post_status(string $new_status, string $old_status, $post): void { @@ -407,11 +422,7 @@ class Level { if (!$comment instanceof \WP_Comment || (int) $comment->user_id <= 0) { return; } - $post = get_post((int) $comment->comment_post_ID); - $key = $post instanceof \WP_Post && $post->post_type === 'aksrt_forum_topic' ? 'bbs_posts_new' : 'comment_new'; - if ($key === 'comment_new') { - self::award_once((int) $comment->user_id, $key, 'comment', (int) $comment->comment_ID, '_aksrt_level_comment_new'); - } + self::award_once((int) $comment->user_id, 'comment_new', 'comment', (int) $comment->comment_ID, '_aksrt_level_comment_new'); } public function on_followed($follower_id, $followed_id): void { @@ -471,7 +482,16 @@ class Level { public function on_forum_scored($topic_id, $author_id, $amount): void { if ((float) $amount > 0) { - self::award_once((int) $author_id, 'bbs_score_extra', 'post', (int) $topic_id, '_aksrt_level_bbs_score_extra', 5); + $score_detail = get_post_meta((int) $topic_id, '_aksrt_score_detail', true); + $score_detail = is_array($score_detail) ? $score_detail : []; + $actor_id = 0; + foreach (array_reverse($score_detail, true) as $candidate => $entry) { + if ((float) ($entry['amount'] ?? 0) === (float) $amount) { + $actor_id = (int) $candidate; + break; + } + } + self::award_once((int) $author_id, 'bbs_score_extra', 'post', (int) $topic_id, '_aksrt_level_bbs_score_extra', 5, $actor_id); } } diff --git a/src/AKSRT/User/LevelAdmin.php b/src/AKSRT/User/LevelAdmin.php index 40d12e31..aa1853e8 100644 --- a/src/AKSRT/User/LevelAdmin.php +++ b/src/AKSRT/User/LevelAdmin.php @@ -109,11 +109,17 @@ class LevelAdmin { - + ID, 'aksrt_user_level_nonce'); ?> + +

+ + + + + -

@@ -130,7 +136,8 @@ class LevelAdmin { return; } $integral = max(0, (int) ($_POST['aksrt_level_integral'] ?? 0)); + $level = max(1, min(Level::settings()['max'], (int) ($_POST['aksrt_user_level'] ?? 1))); update_user_meta($user_id, Level::META_INTEGRAL, $integral); - update_user_meta($user_id, Level::META_LEVEL, Level::resolve_level($integral)); + update_user_meta($user_id, Level::META_LEVEL, $level); } }