fix(user): close level parity gaps

This commit is contained in:
2026-07-30 10:21:48 +08:00
parent a57b44bd61
commit d4946ee554
8 changed files with 136 additions and 38 deletions
+23 -3
View File
@@ -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 + 行为得分键'],
]],
],
],
+24 -12
View File
@@ -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 当日邀请兑现次数限制 */
+1 -1
View File
@@ -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')) {
+5
View File
@@ -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');
}
}
+38 -18
View File
@@ -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);
}
}
+10 -3
View File
@@ -109,11 +109,17 @@ class LevelAdmin {
</tr>
<?php if ($can_manage) : ?>
<tr>
<th><label for="aksrt_level_integral"><?php esc_html_e('累计经验值', 'aksrt'); ?></label></th>
<th><label for="aksrt_user_level"><?php esc_html_e('用户等级', 'aksrt'); ?></label></th>
<td>
<?php wp_nonce_field('aksrt_user_level_admin_' . (int) $user->ID, 'aksrt_user_level_nonce'); ?>
<input type="number" class="small-text" id="aksrt_user_level" name="aksrt_user_level" min="1" max="<?php echo esc_attr((string) Level::settings()['max']); ?>" step="1" value="<?php echo esc_attr((string) $progress['level']); ?>">
<p class="description"><?php esc_html_e('手动设置等级时请同步调整累计经验,使两项符合等级阈值。', 'aksrt'); ?></p>
</td>
</tr>
<tr>
<th><label for="aksrt_level_integral"><?php esc_html_e('累计经验值', 'aksrt'); ?></label></th>
<td>
<input type="number" class="regular-text" id="aksrt_level_integral" name="aksrt_level_integral" min="0" max="9999999999999999" step="1" value="<?php echo esc_attr((string) $progress['integral']); ?>">
<p class="description"><?php esc_html_e('保存后按当前等级阈值重新计算等级。直接替换当前经验,不追加经验流水。', 'aksrt'); ?></p>
</td>
</tr>
<?php endif; ?>
@@ -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);
}
}